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 {
|
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..]);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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("%")
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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")}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue