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:
parent
3019cc7253
commit
67ba5bcecf
|
|
@ -33,7 +33,7 @@ impl LayoutMessageHandler {
|
|||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ impl<'a> Iterator for WidgetIter<'a> {
|
|||
self.current_slice = Some(&self.current_slice.unwrap()[1..]);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -260,7 +260,7 @@ impl<'a> Iterator for WidgetIterMut<'a> {
|
|||
self.current_slice = Some(rest);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,24 +47,16 @@ pub struct PopoverButton {
|
|||
|
||||
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,
|
||||
|
||||
#[serde(rename = "optionsWidget")]
|
||||
pub options_widget: SubLayout,
|
||||
|
||||
#[serde(rename = "popoverMinWidth")]
|
||||
pub popover_min_width: Option<u32>,
|
||||
|
||||
#[serde(skip)]
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ use graphene_core::Color;
|
|||
|
||||
use glam::DAffine2;
|
||||
|
||||
use super::utility_types::misc::{OptionBoundsSnapping, OptionPointSnapping};
|
||||
|
||||
#[impl_message(Message, PortfolioMessage, Document)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DocumentMessage {
|
||||
|
|
@ -126,8 +128,8 @@ pub enum DocumentMessage {
|
|||
},
|
||||
SetSnapping {
|
||||
snapping_enabled: Option<bool>,
|
||||
bounding_box_snapping: Option<bool>,
|
||||
geometry_snapping: Option<bool>,
|
||||
bounding_box_snapping: Option<OptionBoundsSnapping>,
|
||||
geometry_snapping: Option<OptionPointSnapping>,
|
||||
},
|
||||
SetViewMode {
|
||||
view_mode: ViewMode,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH};
|
||||
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 {
|
||||
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 } => {
|
||||
self.view_mode = view_mode;
|
||||
|
|
@ -1149,7 +1193,16 @@ impl DocumentMessageHandler {
|
|||
.tooltip("Overlays")
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into())
|
||||
.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(),
|
||||
CheckboxInput::new(snapping_state.snapping_enabled)
|
||||
.icon("Snapping")
|
||||
|
|
@ -1158,47 +1211,112 @@ impl DocumentMessageHandler {
|
|||
let snapping_enabled = optional_input.checked;
|
||||
DocumentMessage::SetSnapping {
|
||||
snapping_enabled: Some(snapping_enabled),
|
||||
bounding_box_snapping: Some(snapping_state.bounding_box_snapping),
|
||||
geometry_snapping: Some(snapping_state.geometry_snapping),
|
||||
bounding_box_snapping: None,
|
||||
geometry_snapping: None,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
PopoverButton::new("Snapping", "Snap customization settings")
|
||||
.options_widget(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(snapping_state.bounding_box_snapping)
|
||||
.tooltip(SnappingOptions::BoundingBoxes.to_string())
|
||||
.on_update(move |input: &CheckboxInput| {
|
||||
DocumentMessage::SetSnapping {
|
||||
snapping_enabled: None,
|
||||
bounding_box_snapping: Some(input.checked),
|
||||
geometry_snapping: None,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.snapping_state.geometry_snapping)
|
||||
.tooltip(SnappingOptions::Geometry.to_string())
|
||||
.on_update(|input: &CheckboxInput| {
|
||||
DocumentMessage::SetSnapping {
|
||||
snapping_enabled: None,
|
||||
bounding_box_snapping: None,
|
||||
geometry_snapping: Some(input.checked),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new(SnappingOptions::Geometry.to_string()).widget_holder(),
|
||||
],
|
||||
},
|
||||
])
|
||||
PopoverButton::new()
|
||||
.popover_layout(
|
||||
[
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Snapping").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).widget_holder()],
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.chain(
|
||||
[
|
||||
(BoundingBoxSnapTarget::Center, snapping_state.bounds.centers),
|
||||
(BoundingBoxSnapTarget::Corner, snapping_state.bounds.corners),
|
||||
(BoundingBoxSnapTarget::Edge, snapping_state.bounds.edges),
|
||||
(BoundingBoxSnapTarget::EdgeMidpoint, snapping_state.bounds.edge_midpoints),
|
||||
]
|
||||
.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: Some(OptionBoundsSnapping {
|
||||
edges: if enum_type == BoundingBoxSnapTarget::Edge { Some(input.checked) } else { None },
|
||||
edge_midpoints: if enum_type == BoundingBoxSnapTarget::EdgeMidpoint { Some(input.checked) } else { None },
|
||||
centers: if enum_type == BoundingBoxSnapTarget::Center { Some(input.checked) } else { None },
|
||||
corners: if enum_type == BoundingBoxSnapTarget::Corner { Some(input.checked) } else { None },
|
||||
}),
|
||||
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(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
CheckboxInput::new(self.snapping_state.grid_snapping)
|
||||
|
|
@ -1206,8 +1324,8 @@ impl DocumentMessageHandler {
|
|||
.tooltip("Grid")
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisible(optional_input.checked).into())
|
||||
.widget_holder(),
|
||||
PopoverButton::new("Grid", "Grid customization settings")
|
||||
.options_widget(overlay_options(&self.snapping_state.grid))
|
||||
PopoverButton::new()
|
||||
.popover_layout(overlay_options(&self.snapping_state.grid))
|
||||
.popover_min_width(Some(320))
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
|
|
@ -1230,7 +1348,16 @@ impl DocumentMessageHandler {
|
|||
_ => Some(1),
|
||||
})
|
||||
.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(),
|
||||
IconButton::new("ZoomIn", 24)
|
||||
.tooltip("Zoom In")
|
||||
|
|
@ -1247,25 +1374,34 @@ impl DocumentMessageHandler {
|
|||
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::ResetCanvasTiltAndZoomTo100Percent))
|
||||
.on_update(|_| NavigationMessage::ResetCanvasTiltAndZoomTo100Percent.into())
|
||||
.widget_holder(),
|
||||
PopoverButton::new(
|
||||
"Canvas Navigation",
|
||||
"
|
||||
Interactive controls in this\n\
|
||||
menu are coming soon.\n\
|
||||
\n\
|
||||
Pan:\n\
|
||||
• Middle Click Drag\n\
|
||||
\n\
|
||||
Tilt:\n\
|
||||
• Alt + Middle Click Drag\n\
|
||||
\n\
|
||||
Zoom:\n\
|
||||
• Shift + Middle Click Drag\n\
|
||||
• Ctrl + Scroll Wheel Roll
|
||||
"
|
||||
.trim(),
|
||||
)
|
||||
.widget_holder(),
|
||||
PopoverButton::new()
|
||||
.popover_layout(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Canvas Navigation").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(
|
||||
"
|
||||
Interactive controls in this\n\
|
||||
menu are coming soon.\n\
|
||||
\n\
|
||||
Pan:\n\
|
||||
• Middle Click Drag\n\
|
||||
\n\
|
||||
Tilt:\n\
|
||||
• Alt + Middle Click Drag\n\
|
||||
\n\
|
||||
Zoom:\n\
|
||||
• Shift + Middle Click Drag\n\
|
||||
• Ctrl + Scroll Wheel Roll
|
||||
"
|
||||
.trim(),
|
||||
)
|
||||
.multiline(true)
|
||||
.widget_holder()],
|
||||
},
|
||||
])
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(self.navigation_handler.snapped_scale(self.navigation.zoom) * 100.))
|
||||
.unit("%")
|
||||
|
|
|
|||
|
|
@ -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: vec![
|
||||
TextLabel::new("Origin").table_align(true).widget_holder(),
|
||||
|
|
|
|||
|
|
@ -57,8 +57,6 @@ impl DocumentMode {
|
|||
/// SnappingState determines the current individual snapping states
|
||||
pub struct SnappingState {
|
||||
pub snapping_enabled: bool,
|
||||
pub bounding_box_snapping: bool,
|
||||
pub geometry_snapping: bool,
|
||||
pub grid_snapping: bool,
|
||||
pub bounds: BoundsSnapping,
|
||||
pub nodes: PointSnapping,
|
||||
|
|
@ -70,8 +68,6 @@ impl Default for SnappingState {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
snapping_enabled: true,
|
||||
bounding_box_snapping: true,
|
||||
geometry_snapping: true,
|
||||
grid_snapping: false,
|
||||
bounds: BoundsSnapping {
|
||||
edges: true,
|
||||
|
|
@ -82,8 +78,7 @@ impl Default for SnappingState {
|
|||
nodes: PointSnapping {
|
||||
paths: true,
|
||||
path_intersections: true,
|
||||
point_handles_free: true,
|
||||
point_handles_colinear: true,
|
||||
anchors: true,
|
||||
line_midpoints: true,
|
||||
normals: true,
|
||||
tangents: true,
|
||||
|
|
@ -103,15 +98,15 @@ impl SnappingState {
|
|||
return false;
|
||||
}
|
||||
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::Edge => self.bounds.edges,
|
||||
BoundingBoxSnapTarget::EdgeMidpoint => self.bounds.edge_midpoints,
|
||||
BoundingBoxSnapTarget::Center => self.bounds.centers,
|
||||
},
|
||||
SnapTarget::Geometry(nodes) if self.geometry_snapping => match nodes {
|
||||
GeometrySnapTarget::HandlesColinear => self.nodes.point_handles_colinear,
|
||||
GeometrySnapTarget::HandlesFree => self.nodes.point_handles_free,
|
||||
SnapTarget::Geometry(nodes) => match nodes {
|
||||
GeometrySnapTarget::AnchorWithColinearHandles => self.nodes.anchors,
|
||||
GeometrySnapTarget::AnchorWithFreeHandles => self.nodes.anchors,
|
||||
GeometrySnapTarget::LineMidpoint => self.nodes.line_midpoints,
|
||||
GeometrySnapTarget::Path => self.nodes.paths,
|
||||
GeometrySnapTarget::Normal => self.nodes.normals,
|
||||
|
|
@ -131,16 +126,31 @@ pub struct BoundsSnapping {
|
|||
pub edge_midpoints: 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)]
|
||||
pub struct PointSnapping {
|
||||
pub paths: bool,
|
||||
pub path_intersections: bool,
|
||||
pub point_handles_free: bool,
|
||||
pub point_handles_colinear: bool,
|
||||
pub anchors: bool,
|
||||
pub line_midpoints: bool,
|
||||
pub normals: 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)]
|
||||
pub enum GridType {
|
||||
Rectangle { spacing: DVec2 },
|
||||
|
|
@ -216,8 +226,8 @@ impl GridSnapping {
|
|||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BoundingBoxSnapSource {
|
||||
Corner,
|
||||
Center,
|
||||
Corner,
|
||||
EdgeMidpoint,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -227,11 +237,11 @@ pub enum BoardSnapSource {
|
|||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GeometrySnapSource {
|
||||
HandlesColinear,
|
||||
HandlesFree,
|
||||
LineMidpoint,
|
||||
PathIntersection,
|
||||
AnchorWithColinearHandles,
|
||||
AnchorWithFreeHandles,
|
||||
Handle,
|
||||
LineMidpoint,
|
||||
Intersection,
|
||||
}
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SnapSource {
|
||||
|
|
@ -249,23 +259,50 @@ impl SnapSource {
|
|||
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 {
|
||||
Center,
|
||||
Corner,
|
||||
Edge,
|
||||
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 {
|
||||
HandlesColinear,
|
||||
HandlesFree,
|
||||
AnchorWithColinearHandles,
|
||||
AnchorWithFreeHandles,
|
||||
LineMidpoint,
|
||||
Path,
|
||||
Normal,
|
||||
Tangent,
|
||||
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)]
|
||||
pub enum BoardSnapTarget {
|
||||
Edge,
|
||||
|
|
|
|||
|
|
@ -278,9 +278,9 @@ impl ShapeState {
|
|||
let source = if handle.is_handle() {
|
||||
SnapSource::Geometry(GeometrySnapSource::Handle)
|
||||
} else if are_manipulator_handles_colinear(group, to_document, subpath, index) {
|
||||
SnapSource::Geometry(GeometrySnapSource::HandlesColinear)
|
||||
SnapSource::Geometry(GeometrySnapSource::AnchorWithColinearHandles)
|
||||
} else {
|
||||
SnapSource::Geometry(GeometrySnapSource::HandlesFree)
|
||||
SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles)
|
||||
};
|
||||
let Some(position) = handle.get_position(group) else { continue };
|
||||
let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source);
|
||||
|
|
|
|||
|
|
@ -327,10 +327,10 @@ impl SnapCandidatePoint {
|
|||
Self::new(document_point, source, SnapTarget::None)
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
if colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::HandlesColinear)) {
|
||||
if colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::AnchorWithColinearHandles)) {
|
||||
// Colinear handles
|
||||
points.push(SnapCandidatePoint::new(
|
||||
to_document.transform_point2(group.anchor),
|
||||
SnapSource::Geometry(GeometrySnapSource::HandlesColinear),
|
||||
SnapTarget::Geometry(GeometrySnapTarget::HandlesColinear),
|
||||
SnapSource::Geometry(GeometrySnapSource::AnchorWithColinearHandles),
|
||||
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
|
||||
points.push(SnapCandidatePoint::new(
|
||||
to_document.transform_point2(group.anchor),
|
||||
SnapSource::Geometry(GeometrySnapSource::HandlesFree),
|
||||
SnapTarget::Geometry(GeometrySnapTarget::HandlesFree),
|
||||
SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles),
|
||||
SnapTarget::Geometry(GeometrySnapTarget::AnchorWithFreeHandles),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,7 +171,19 @@ impl LayoutHolder for SelectTool {
|
|||
let disabled = self.tool_data.selected_layers_count < 2;
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
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
|
||||
let disabled = self.tool_data.selected_layers_count == 0;
|
||||
|
|
|
|||
|
|
@ -134,13 +134,8 @@
|
|||
{/if}
|
||||
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
|
||||
{#if popoverButton}
|
||||
<PopoverButton {...exclude(popoverButton, ["header", "text", "optionsWidget"])}>
|
||||
<TextLabel bold={true}>{popoverButton.header}</TextLabel>
|
||||
{#if popoverButton.optionsWidget?.length}
|
||||
<WidgetLayout layout={{ layout: popoverButton.optionsWidget, layoutTarget: layoutTarget }} />
|
||||
{:else}
|
||||
<TextLabel multiline={true}>{popoverButton.text}</TextLabel>
|
||||
{/if}
|
||||
<PopoverButton {...exclude(popoverButton, ["popoverLayout"])}>
|
||||
<WidgetLayout layout={{ layout: popoverButton.popoverLayout, layoutTarget: layoutTarget }} />
|
||||
</PopoverButton>
|
||||
{/if}
|
||||
{@const radioInput = narrowWidgetProps(component.props, "RadioInput")}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,16 @@
|
|||
.floating-menu {
|
||||
left: 50%;
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -870,17 +870,13 @@ export class PopoverButton extends WidgetProps {
|
|||
|
||||
disabled!: boolean;
|
||||
|
||||
// Body
|
||||
header!: string;
|
||||
|
||||
text!: string;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
|
||||
popoverMinWidth: number | undefined;
|
||||
// Body
|
||||
popoverLayout!: LayoutGroup[];
|
||||
|
||||
optionsWidget: LayoutGroup[] | undefined;
|
||||
popoverMinWidth: number | undefined;
|
||||
}
|
||||
|
||||
export type RadioEntryData = {
|
||||
|
|
@ -1086,7 +1082,7 @@ function hoistWidgetHolder(widgetHolder: any): Widget {
|
|||
props.kind = kind;
|
||||
|
||||
if (kind === "PopoverButton") {
|
||||
props.optionsWidget = props.optionsWidget.map(createLayoutGroup);
|
||||
props.popoverLayout = props.popoverLayout.map(createLayoutGroup);
|
||||
}
|
||||
|
||||
const { widgetId } = widgetHolder;
|
||||
|
|
@ -1136,8 +1132,8 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
|
|||
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
|
||||
if ("layout" in targetLayout) return targetLayout.layout[index];
|
||||
if (targetLayout instanceof Widget) {
|
||||
if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.optionsWidget) {
|
||||
return targetLayout.props.optionsWidget[index];
|
||||
if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) {
|
||||
return targetLayout.props.popoverLayout[index];
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Tried to index widget");
|
||||
|
|
|
|||
Loading…
Reference in New Issue