Added fine-grained choices to Snapping options popover (#1730)

* Added Customised Snapping

* Fix snapping choice to Anchors; clean up popover menu code

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Shyam Jayakannan 2024-04-18 09:54:15 +05:30 committed by GitHub
parent 3019cc7253
commit 67ba5bcecf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 318 additions and 135 deletions

View File

@ -33,7 +33,7 @@ impl LayoutMessageHandler {
} }
if let Widget::PopoverButton(popover) = &widget.widget { if let Widget::PopoverButton(popover) = &widget.widget {
stack.extend(popover.options_widget.iter().enumerate().map(|(child, val)| ([widget_path.as_slice(), &[index, child]].concat(), val))); stack.extend(popover.popover_layout.iter().enumerate().map(|(child, val)| ([widget_path.as_slice(), &[index, child]].concat(), val)));
} }
} }
} }

View File

@ -219,7 +219,7 @@ impl<'a> Iterator for WidgetIter<'a> {
self.current_slice = Some(&self.current_slice.unwrap()[1..]); self.current_slice = Some(&self.current_slice.unwrap()[1..]);
if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = item { if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = item {
self.stack.extend(p.options_widget.iter()); self.stack.extend(p.popover_layout.iter());
return self.next(); return self.next();
} }
@ -260,7 +260,7 @@ impl<'a> Iterator for WidgetIterMut<'a> {
self.current_slice = Some(rest); self.current_slice = Some(rest);
if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = first { if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = first {
self.stack.extend(p.options_widget.iter_mut()); self.stack.extend(p.popover_layout.iter_mut());
return self.next(); return self.next();
} }

View File

@ -47,24 +47,16 @@ pub struct PopoverButton {
pub disabled: bool, pub disabled: bool,
// Placeholder popover content heading
#[widget_builder(constructor)]
pub header: String,
// Placeholder popover content paragraph
#[widget_builder(constructor)]
pub text: String,
pub tooltip: String, pub tooltip: String,
#[serde(rename = "optionsWidget")]
pub options_widget: SubLayout,
#[serde(rename = "popoverMinWidth")]
pub popover_min_width: Option<u32>,
#[serde(skip)] #[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>, pub tooltip_shortcut: Option<ActionKeys>,
#[serde(rename = "popoverLayout")]
pub popover_layout: SubLayout,
#[serde(rename = "popoverMinWidth")]
pub popover_min_width: Option<u32>,
} }
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] #[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]

View File

@ -12,6 +12,8 @@ use graphene_core::Color;
use glam::DAffine2; use glam::DAffine2;
use super::utility_types::misc::{OptionBoundsSnapping, OptionPointSnapping};
#[impl_message(Message, PortfolioMessage, Document)] #[impl_message(Message, PortfolioMessage, Document)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum DocumentMessage { pub enum DocumentMessage {
@ -126,8 +128,8 @@ pub enum DocumentMessage {
}, },
SetSnapping { SetSnapping {
snapping_enabled: Option<bool>, snapping_enabled: Option<bool>,
bounding_box_snapping: Option<bool>, bounding_box_snapping: Option<OptionBoundsSnapping>,
geometry_snapping: Option<bool>, geometry_snapping: Option<OptionPointSnapping>,
}, },
SetViewMode { SetViewMode {
view_mode: ViewMode, view_mode: ViewMode,

View File

@ -1,5 +1,5 @@
use super::utility_types::error::EditorError; use super::utility_types::error::EditorError;
use super::utility_types::misc::{SnappingOptions, SnappingState}; use super::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, OptionBoundsSnapping, OptionPointSnapping, SnappingOptions, SnappingState};
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes}; use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH}; use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH};
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING}; use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING};
@ -718,12 +718,56 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
if let Some(state) = snapping_enabled { if let Some(state) = snapping_enabled {
self.snapping_state.snapping_enabled = state self.snapping_state.snapping_enabled = state
}; };
if let Some(state) = bounding_box_snapping {
self.snapping_state.bounding_box_snapping = state if let Some(OptionBoundsSnapping {
edge_midpoints,
edges,
centers,
corners,
}) = bounding_box_snapping
{
if let Some(state) = edge_midpoints {
self.snapping_state.bounds.edge_midpoints = state
};
if let Some(state) = edges {
self.snapping_state.bounds.edges = state
};
if let Some(state) = centers {
self.snapping_state.bounds.centers = state
};
if let Some(state) = corners {
self.snapping_state.bounds.corners = state
};
}
if let Some(OptionPointSnapping {
paths,
path_intersections,
anchors,
line_midpoints,
normals,
tangents,
}) = geometry_snapping
{
if let Some(state) = path_intersections {
self.snapping_state.nodes.path_intersections = state
};
if let Some(state) = paths {
self.snapping_state.nodes.paths = state
};
if let Some(state) = anchors {
self.snapping_state.nodes.anchors = state
};
if let Some(state) = line_midpoints {
self.snapping_state.nodes.line_midpoints = state
};
if let Some(state) = normals {
self.snapping_state.nodes.normals = state
};
if let Some(state) = tangents {
self.snapping_state.nodes.tangents = state
};
} }
if let Some(state) = geometry_snapping {
self.snapping_state.geometry_snapping = state
};
} }
DocumentMessage::SetViewMode { view_mode } => { DocumentMessage::SetViewMode { view_mode } => {
self.view_mode = view_mode; self.view_mode = view_mode;
@ -1149,7 +1193,16 @@ impl DocumentMessageHandler {
.tooltip("Overlays") .tooltip("Overlays")
.on_update(|optional_input: &CheckboxInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()) .on_update(|optional_input: &CheckboxInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into())
.widget_holder(), .widget_holder(),
PopoverButton::new("Overlays", "Coming soon").widget_holder(), PopoverButton::new()
.popover_layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Overlays").bold(true).widget_holder()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new("Coming soon").widget_holder()],
},
])
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
CheckboxInput::new(snapping_state.snapping_enabled) CheckboxInput::new(snapping_state.snapping_enabled)
.icon("Snapping") .icon("Snapping")
@ -1158,47 +1211,112 @@ impl DocumentMessageHandler {
let snapping_enabled = optional_input.checked; let snapping_enabled = optional_input.checked;
DocumentMessage::SetSnapping { DocumentMessage::SetSnapping {
snapping_enabled: Some(snapping_enabled), snapping_enabled: Some(snapping_enabled),
bounding_box_snapping: Some(snapping_state.bounding_box_snapping), bounding_box_snapping: None,
geometry_snapping: Some(snapping_state.geometry_snapping), geometry_snapping: None,
} }
.into() .into()
}) })
.widget_holder(), .widget_holder(),
PopoverButton::new("Snapping", "Snap customization settings") PopoverButton::new()
.options_widget(vec![ .popover_layout(
LayoutGroup::Row { [
widgets: vec![ LayoutGroup::Row {
CheckboxInput::new(snapping_state.bounding_box_snapping) widgets: vec![TextLabel::new("Snapping").bold(true).widget_holder()],
.tooltip(SnappingOptions::BoundingBoxes.to_string()) },
.on_update(move |input: &CheckboxInput| { LayoutGroup::Row {
DocumentMessage::SetSnapping { widgets: vec![TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).widget_holder()],
snapping_enabled: None, },
bounding_box_snapping: Some(input.checked), ]
geometry_snapping: None, .into_iter()
} .chain(
.into() [
}) (BoundingBoxSnapTarget::Center, snapping_state.bounds.centers),
.widget_holder(), (BoundingBoxSnapTarget::Corner, snapping_state.bounds.corners),
TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).widget_holder(), (BoundingBoxSnapTarget::Edge, snapping_state.bounds.edges),
], (BoundingBoxSnapTarget::EdgeMidpoint, snapping_state.bounds.edge_midpoints),
}, ]
LayoutGroup::Row { .into_iter()
widgets: vec![ .map(|(enum_type, bound_state)| LayoutGroup::Row {
CheckboxInput::new(self.snapping_state.geometry_snapping) widgets: vec![
.tooltip(SnappingOptions::Geometry.to_string()) CheckboxInput::new(bound_state)
.on_update(|input: &CheckboxInput| { .on_update(move |input: &CheckboxInput| {
DocumentMessage::SetSnapping { DocumentMessage::SetSnapping {
snapping_enabled: None, snapping_enabled: None,
bounding_box_snapping: None, bounding_box_snapping: Some(OptionBoundsSnapping {
geometry_snapping: Some(input.checked), edges: if enum_type == BoundingBoxSnapTarget::Edge { Some(input.checked) } else { None },
} edge_midpoints: if enum_type == BoundingBoxSnapTarget::EdgeMidpoint { Some(input.checked) } else { None },
.into() centers: if enum_type == BoundingBoxSnapTarget::Center { Some(input.checked) } else { None },
}) corners: if enum_type == BoundingBoxSnapTarget::Corner { Some(input.checked) } else { None },
.widget_holder(), }),
TextLabel::new(SnappingOptions::Geometry.to_string()).widget_holder(), geometry_snapping: None,
], }
}, .into()
]) })
.widget_holder(),
TextLabel::new(enum_type.to_string()).widget_holder(),
],
})
.chain(
[
LayoutGroup::Row {
widgets: vec![TextLabel::new(SnappingOptions::Geometry.to_string()).widget_holder()],
},
LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(snapping_state.nodes.anchors)
.on_update(move |input: &CheckboxInput| {
DocumentMessage::SetSnapping {
snapping_enabled: None,
bounding_box_snapping: None,
geometry_snapping: Some(OptionPointSnapping {
anchors: Some(input.checked),
..Default::default()
}),
}
.into()
})
.widget_holder(),
TextLabel::new("Anchor").widget_holder(),
],
},
]
.into_iter()
.chain(
[
(GeometrySnapTarget::LineMidpoint, snapping_state.nodes.line_midpoints),
(GeometrySnapTarget::Path, snapping_state.nodes.paths),
(GeometrySnapTarget::Normal, snapping_state.nodes.normals),
(GeometrySnapTarget::Tangent, snapping_state.nodes.tangents),
(GeometrySnapTarget::Intersection, snapping_state.nodes.path_intersections),
]
.into_iter()
.map(|(enum_type, bound_state)| LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(bound_state)
.on_update(move |input: &CheckboxInput| {
DocumentMessage::SetSnapping {
snapping_enabled: None,
bounding_box_snapping: None,
geometry_snapping: Some(OptionPointSnapping {
anchors: None,
line_midpoints: if enum_type == GeometrySnapTarget::LineMidpoint { Some(input.checked) } else { None },
paths: if enum_type == GeometrySnapTarget::Path { Some(input.checked) } else { None },
normals: if enum_type == GeometrySnapTarget::Normal { Some(input.checked) } else { None },
tangents: if enum_type == GeometrySnapTarget::Tangent { Some(input.checked) } else { None },
path_intersections: if enum_type == GeometrySnapTarget::Intersection { Some(input.checked) } else { None },
}),
}
.into()
})
.widget_holder(),
TextLabel::new(enum_type.to_string()).widget_holder(),
],
}),
),
),
)
.collect(),
)
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
CheckboxInput::new(self.snapping_state.grid_snapping) CheckboxInput::new(self.snapping_state.grid_snapping)
@ -1206,8 +1324,8 @@ impl DocumentMessageHandler {
.tooltip("Grid") .tooltip("Grid")
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisible(optional_input.checked).into()) .on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisible(optional_input.checked).into())
.widget_holder(), .widget_holder(),
PopoverButton::new("Grid", "Grid customization settings") PopoverButton::new()
.options_widget(overlay_options(&self.snapping_state.grid)) .popover_layout(overlay_options(&self.snapping_state.grid))
.popover_min_width(Some(320)) .popover_min_width(Some(320))
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(),
@ -1230,7 +1348,16 @@ impl DocumentMessageHandler {
_ => Some(1), _ => Some(1),
}) })
.widget_holder(), .widget_holder(),
PopoverButton::new("View Mode", "Coming soon").widget_holder(), PopoverButton::new()
.popover_layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("View Mode").bold(true).widget_holder()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new("Coming soon").widget_holder()],
},
])
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(),
IconButton::new("ZoomIn", 24) IconButton::new("ZoomIn", 24)
.tooltip("Zoom In") .tooltip("Zoom In")
@ -1247,25 +1374,34 @@ impl DocumentMessageHandler {
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::ResetCanvasTiltAndZoomTo100Percent)) .tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::ResetCanvasTiltAndZoomTo100Percent))
.on_update(|_| NavigationMessage::ResetCanvasTiltAndZoomTo100Percent.into()) .on_update(|_| NavigationMessage::ResetCanvasTiltAndZoomTo100Percent.into())
.widget_holder(), .widget_holder(),
PopoverButton::new( PopoverButton::new()
"Canvas Navigation", .popover_layout(vec![
" LayoutGroup::Row {
Interactive controls in this\n\ widgets: vec![TextLabel::new("Canvas Navigation").bold(true).widget_holder()],
menu are coming soon.\n\ },
\n\ LayoutGroup::Row {
Pan:\n\ widgets: vec![TextLabel::new(
Middle Click Drag\n\ "
\n\ Interactive controls in this\n\
Tilt:\n\ menu are coming soon.\n\
Alt + Middle Click Drag\n\ \n\
\n\ Pan:\n\
Zoom:\n\ Middle Click Drag\n\
Shift + Middle Click Drag\n\ \n\
Ctrl + Scroll Wheel Roll Tilt:\n\
" Alt + Middle Click Drag\n\
.trim(), \n\
) Zoom:\n\
.widget_holder(), Shift + Middle Click Drag\n\
Ctrl + Scroll Wheel Roll
"
.trim(),
)
.multiline(true)
.widget_holder()],
},
])
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(self.navigation_handler.snapped_scale(self.navigation.zoom) * 100.)) NumberInput::new(Some(self.navigation_handler.snapped_scale(self.navigation.zoom) * 100.))
.unit("%") .unit("%")

View File

@ -105,6 +105,9 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
} }
}) })
}; };
widgets.push(LayoutGroup::Row {
widgets: vec![TextLabel::new("Grid").bold(true).widget_holder()],
});
widgets.push(LayoutGroup::Row { widgets.push(LayoutGroup::Row {
widgets: vec![ widgets: vec![
TextLabel::new("Origin").table_align(true).widget_holder(), TextLabel::new("Origin").table_align(true).widget_holder(),

View File

@ -57,8 +57,6 @@ impl DocumentMode {
/// SnappingState determines the current individual snapping states /// SnappingState determines the current individual snapping states
pub struct SnappingState { pub struct SnappingState {
pub snapping_enabled: bool, pub snapping_enabled: bool,
pub bounding_box_snapping: bool,
pub geometry_snapping: bool,
pub grid_snapping: bool, pub grid_snapping: bool,
pub bounds: BoundsSnapping, pub bounds: BoundsSnapping,
pub nodes: PointSnapping, pub nodes: PointSnapping,
@ -70,8 +68,6 @@ impl Default for SnappingState {
fn default() -> Self { fn default() -> Self {
Self { Self {
snapping_enabled: true, snapping_enabled: true,
bounding_box_snapping: true,
geometry_snapping: true,
grid_snapping: false, grid_snapping: false,
bounds: BoundsSnapping { bounds: BoundsSnapping {
edges: true, edges: true,
@ -82,8 +78,7 @@ impl Default for SnappingState {
nodes: PointSnapping { nodes: PointSnapping {
paths: true, paths: true,
path_intersections: true, path_intersections: true,
point_handles_free: true, anchors: true,
point_handles_colinear: true,
line_midpoints: true, line_midpoints: true,
normals: true, normals: true,
tangents: true, tangents: true,
@ -103,15 +98,15 @@ impl SnappingState {
return false; return false;
} }
match target { match target {
SnapTarget::BoundingBox(bounding_box) if self.bounding_box_snapping => match bounding_box { SnapTarget::BoundingBox(bounding_box) => match bounding_box {
BoundingBoxSnapTarget::Corner => self.bounds.corners, BoundingBoxSnapTarget::Corner => self.bounds.corners,
BoundingBoxSnapTarget::Edge => self.bounds.edges, BoundingBoxSnapTarget::Edge => self.bounds.edges,
BoundingBoxSnapTarget::EdgeMidpoint => self.bounds.edge_midpoints, BoundingBoxSnapTarget::EdgeMidpoint => self.bounds.edge_midpoints,
BoundingBoxSnapTarget::Center => self.bounds.centers, BoundingBoxSnapTarget::Center => self.bounds.centers,
}, },
SnapTarget::Geometry(nodes) if self.geometry_snapping => match nodes { SnapTarget::Geometry(nodes) => match nodes {
GeometrySnapTarget::HandlesColinear => self.nodes.point_handles_colinear, GeometrySnapTarget::AnchorWithColinearHandles => self.nodes.anchors,
GeometrySnapTarget::HandlesFree => self.nodes.point_handles_free, GeometrySnapTarget::AnchorWithFreeHandles => self.nodes.anchors,
GeometrySnapTarget::LineMidpoint => self.nodes.line_midpoints, GeometrySnapTarget::LineMidpoint => self.nodes.line_midpoints,
GeometrySnapTarget::Path => self.nodes.paths, GeometrySnapTarget::Path => self.nodes.paths,
GeometrySnapTarget::Normal => self.nodes.normals, GeometrySnapTarget::Normal => self.nodes.normals,
@ -131,16 +126,31 @@ pub struct BoundsSnapping {
pub edge_midpoints: bool, pub edge_midpoints: bool,
pub centers: bool, pub centers: bool,
} }
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct OptionBoundsSnapping {
pub edges: Option<bool>,
pub corners: Option<bool>,
pub edge_midpoints: Option<bool>,
pub centers: Option<bool>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PointSnapping { pub struct PointSnapping {
pub paths: bool, pub paths: bool,
pub path_intersections: bool, pub path_intersections: bool,
pub point_handles_free: bool, pub anchors: bool,
pub point_handles_colinear: bool,
pub line_midpoints: bool, pub line_midpoints: bool,
pub normals: bool, pub normals: bool,
pub tangents: bool, pub tangents: bool,
} }
#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct OptionPointSnapping {
pub paths: Option<bool>,
pub path_intersections: Option<bool>,
pub anchors: Option<bool>,
pub line_midpoints: Option<bool>,
pub normals: Option<bool>,
pub tangents: Option<bool>,
}
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)] #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum GridType { pub enum GridType {
Rectangle { spacing: DVec2 }, Rectangle { spacing: DVec2 },
@ -216,8 +226,8 @@ impl GridSnapping {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundingBoxSnapSource { pub enum BoundingBoxSnapSource {
Corner,
Center, Center,
Corner,
EdgeMidpoint, EdgeMidpoint,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -227,11 +237,11 @@ pub enum BoardSnapSource {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeometrySnapSource { pub enum GeometrySnapSource {
HandlesColinear, AnchorWithColinearHandles,
HandlesFree, AnchorWithFreeHandles,
LineMidpoint,
PathIntersection,
Handle, Handle,
LineMidpoint,
Intersection,
} }
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapSource { pub enum SnapSource {
@ -249,23 +259,50 @@ impl SnapSource {
matches!(self, Self::BoundingBox(_) | Self::Board(_)) matches!(self, Self::BoundingBox(_) | Self::Board(_))
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum BoundingBoxSnapTarget { pub enum BoundingBoxSnapTarget {
Center,
Corner, Corner,
Edge, Edge,
EdgeMidpoint, EdgeMidpoint,
Center,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
impl fmt::Display for BoundingBoxSnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Center => write!(f, "Box Center"),
Self::Corner => write!(f, "Box Corner"),
Self::Edge => write!(f, "Along Edge"),
Self::EdgeMidpoint => write!(f, "Midpoint of Edge"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum GeometrySnapTarget { pub enum GeometrySnapTarget {
HandlesColinear, AnchorWithColinearHandles,
HandlesFree, AnchorWithFreeHandles,
LineMidpoint, LineMidpoint,
Path, Path,
Normal, Normal,
Tangent, Tangent,
Intersection, Intersection,
} }
impl fmt::Display for GeometrySnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AnchorWithColinearHandles => write!(f, "Anchor (Colinear Handles)"),
Self::AnchorWithFreeHandles => write!(f, "Anchor (Free Handles)"),
Self::LineMidpoint => write!(f, "Line Midpoint"),
Self::Path => write!(f, "Path"),
Self::Normal => write!(f, "Normal to Path"),
Self::Tangent => write!(f, "Tangent to Path"),
Self::Intersection => write!(f, "Intersection"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoardSnapTarget { pub enum BoardSnapTarget {
Edge, Edge,

View File

@ -278,9 +278,9 @@ impl ShapeState {
let source = if handle.is_handle() { let source = if handle.is_handle() {
SnapSource::Geometry(GeometrySnapSource::Handle) SnapSource::Geometry(GeometrySnapSource::Handle)
} else if are_manipulator_handles_colinear(group, to_document, subpath, index) { } else if are_manipulator_handles_colinear(group, to_document, subpath, index) {
SnapSource::Geometry(GeometrySnapSource::HandlesColinear) SnapSource::Geometry(GeometrySnapSource::AnchorWithColinearHandles)
} else { } else {
SnapSource::Geometry(GeometrySnapSource::HandlesFree) SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles)
}; };
let Some(position) = handle.get_position(group) else { continue }; let Some(position) = handle.get_position(group) else { continue };
let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source); let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source);

View File

@ -327,10 +327,10 @@ impl SnapCandidatePoint {
Self::new(document_point, source, SnapTarget::None) Self::new(document_point, source, SnapTarget::None)
} }
pub fn handle(document_point: DVec2) -> Self { pub fn handle(document_point: DVec2) -> Self {
Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::HandlesFree)) Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles))
} }
pub fn handle_neighbors(document_point: DVec2, neighbors: impl Into<Vec<DVec2>>) -> Self { pub fn handle_neighbors(document_point: DVec2, neighbors: impl Into<Vec<DVec2>>) -> Self {
let mut point = Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::HandlesFree)); let mut point = Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles));
point.neighbors = neighbors.into(); point.neighbors = neighbors.into();
point point
} }
@ -401,19 +401,19 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
let colinear = are_manipulator_handles_colinear(group, to_document, subpath, index); let colinear = are_manipulator_handles_colinear(group, to_document, subpath, index);
if colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::HandlesColinear)) { if colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::AnchorWithColinearHandles)) {
// Colinear handles // Colinear handles
points.push(SnapCandidatePoint::new( points.push(SnapCandidatePoint::new(
to_document.transform_point2(group.anchor), to_document.transform_point2(group.anchor),
SnapSource::Geometry(GeometrySnapSource::HandlesColinear), SnapSource::Geometry(GeometrySnapSource::AnchorWithColinearHandles),
SnapTarget::Geometry(GeometrySnapTarget::HandlesColinear), SnapTarget::Geometry(GeometrySnapTarget::AnchorWithColinearHandles),
)); ));
} else if !colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::HandlesFree)) { } else if !colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::AnchorWithFreeHandles)) {
// Free handles // Free handles
points.push(SnapCandidatePoint::new( points.push(SnapCandidatePoint::new(
to_document.transform_point2(group.anchor), to_document.transform_point2(group.anchor),
SnapSource::Geometry(GeometrySnapSource::HandlesFree), SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles),
SnapTarget::Geometry(GeometrySnapTarget::HandlesFree), SnapTarget::Geometry(GeometrySnapTarget::AnchorWithFreeHandles),
)); ));
} }
} }

View File

@ -171,7 +171,19 @@ impl LayoutHolder for SelectTool {
let disabled = self.tool_data.selected_layers_count < 2; let disabled = self.tool_data.selected_layers_count < 2;
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.extend(self.alignment_widgets(disabled)); widgets.extend(self.alignment_widgets(disabled));
widgets.push(PopoverButton::new("Align", "Coming soon").disabled(disabled).widget_holder()); widgets.push(
PopoverButton::new()
.popover_layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Align").bold(true).widget_holder()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new("Coming soon").widget_holder()],
},
])
.disabled(disabled)
.widget_holder(),
);
// Flip // Flip
let disabled = self.tool_data.selected_layers_count == 0; let disabled = self.tool_data.selected_layers_count == 0;

View File

@ -134,13 +134,8 @@
{/if} {/if}
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")} {@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
{#if popoverButton} {#if popoverButton}
<PopoverButton {...exclude(popoverButton, ["header", "text", "optionsWidget"])}> <PopoverButton {...exclude(popoverButton, ["popoverLayout"])}>
<TextLabel bold={true}>{popoverButton.header}</TextLabel> <WidgetLayout layout={{ layout: popoverButton.popoverLayout, layoutTarget: layoutTarget }} />
{#if popoverButton.optionsWidget?.length}
<WidgetLayout layout={{ layout: popoverButton.optionsWidget, layoutTarget: layoutTarget }} />
{:else}
<TextLabel multiline={true}>{popoverButton.text}</TextLabel>
{/if}
</PopoverButton> </PopoverButton>
{/if} {/if}
{@const radioInput = narrowWidgetProps(component.props, "RadioInput")} {@const radioInput = narrowWidgetProps(component.props, "RadioInput")}

View File

@ -75,6 +75,16 @@
.floating-menu { .floating-menu {
left: 50%; left: 50%;
bottom: 0; bottom: 0;
.floating-menu-content > :first-child:not(:has(:not(.text-label))),
.floating-menu-content > :first-child:not(:has(:not(.checkbox-input))) {
margin-top: -8px;
}
.floating-menu-content > :last-child:not(:has(:not(.text-label))),
.floating-menu-content > :last-child:not(:has(:not(.checkbox-input))) {
margin-bottom: -8px;
}
} }
} }
</style> </style>

View File

@ -870,17 +870,13 @@ export class PopoverButton extends WidgetProps {
disabled!: boolean; disabled!: boolean;
// Body
header!: string;
text!: string;
@Transform(({ value }: { value: string }) => value || undefined) @Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined; tooltip!: string | undefined;
popoverMinWidth: number | undefined; // Body
popoverLayout!: LayoutGroup[];
optionsWidget: LayoutGroup[] | undefined; popoverMinWidth: number | undefined;
} }
export type RadioEntryData = { export type RadioEntryData = {
@ -1086,7 +1082,7 @@ function hoistWidgetHolder(widgetHolder: any): Widget {
props.kind = kind; props.kind = kind;
if (kind === "PopoverButton") { if (kind === "PopoverButton") {
props.optionsWidget = props.optionsWidget.map(createLayoutGroup); props.popoverLayout = props.popoverLayout.map(createLayoutGroup);
} }
const { widgetId } = widgetHolder; const { widgetId } = widgetHolder;
@ -1136,8 +1132,8 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index]; if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
if ("layout" in targetLayout) return targetLayout.layout[index]; if ("layout" in targetLayout) return targetLayout.layout[index];
if (targetLayout instanceof Widget) { if (targetLayout instanceof Widget) {
if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.optionsWidget) { if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) {
return targetLayout.props.optionsWidget[index]; return targetLayout.props.popoverLayout[index];
} }
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("Tried to index widget"); console.error("Tried to index widget");