Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions editor/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ impl Dispatcher {
menu_bar_message_handler.canvas_tilted = document.document_ptz.tilt() != 0.;
menu_bar_message_handler.canvas_flipped = document.document_ptz.flip;
menu_bar_message_handler.rulers_visible = document.rulers_visible;
menu_bar_message_handler.guides_visible = document.guides_visible;
menu_bar_message_handler.node_graph_open = document.is_graph_overlay_open();
menu_bar_message_handler.has_selected_nodes = selected_nodes.selected_nodes().next().is_some();
menu_bar_message_handler.has_selected_layers = selected_nodes.selected_visible_layers(&document.network_interface).next().is_some();
Expand All @@ -268,6 +269,7 @@ impl Dispatcher {
menu_bar_message_handler.canvas_tilted = false;
menu_bar_message_handler.canvas_flipped = false;
menu_bar_message_handler.rulers_visible = false;
menu_bar_message_handler.guides_visible = false;
menu_bar_message_handler.node_graph_open = false;
menu_bar_message_handler.has_selected_nodes = false;
menu_bar_message_handler.has_selected_layers = false;
Expand Down
6 changes: 6 additions & 0 deletions editor/src/messages/menu_bar/menu_bar_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct MenuBarMessageHandler {
pub canvas_tilted: bool,
pub canvas_flipped: bool,
pub rulers_visible: bool,
pub guides_visible: bool,
pub node_graph_open: bool,
pub has_selected_nodes: bool,
pub has_selected_layers: bool,
Expand Down Expand Up @@ -614,6 +615,11 @@ impl LayoutHolder for MenuBarMessageHandler {
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleRulers))
.on_commit(|_| PortfolioMessage::ToggleRulers.into())
.disabled(no_active_document),
MenuListEntry::new("Guides")
.label("Guides")
.icon(if self.guides_visible { "CheckboxChecked" } else { "CheckboxUnchecked" })
.on_commit(|_| DocumentMessage::ToggleGuidesVisibility.into())
.disabled(no_active_document),
],
])
.widget_instance(),
Expand Down
21 changes: 21 additions & 0 deletions editor/src/messages/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::guide::{GuideDirection, GuideId};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping};
use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::prelude::*;
Expand Down Expand Up @@ -82,6 +83,26 @@ pub enum DocumentMessage {
GridVisibility {
visible: bool,
},
// Guide messages
CreateGuide {
id: GuideId,
direction: GuideDirection,
position: f64,
},
MoveGuide {
id: GuideId,
position: f64,
},
DeleteGuide {
id: GuideId,
},
GuideOverlays {
context: OverlayContext,
},
ToggleGuidesVisibility,
SetHoveredGuide {
id: Option<GuideId>,
},
GroupSelectedLayers {
group_folder_type: GroupFolderType,
},
Expand Down
100 changes: 100 additions & 0 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::node_graph::document_node_definitions;
use super::node_graph::utility_types::Transform;
use super::utility_types::error::EditorError;
use super::utility_types::guide::{Guide, GuideId};
use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS, SnappingOptions, SnappingState};
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
Expand Down Expand Up @@ -138,6 +139,18 @@ pub struct DocumentMessageHandler {
/// If the user clicks or Ctrl-clicks one layer, it becomes the start of the range selection and then Shift-clicking another layer selects all layers between the start and end.
#[serde(skip)]
layer_range_selection_reference: Option<LayerNodeIdentifier>,
/// List of horizontal guide lines in document space.
#[serde(default)]
pub horizontal_guides: Vec<Guide>,
/// List of vertical guide lines in document space.
#[serde(default)]
pub vertical_guides: Vec<Guide>,
/// Whether guide lines are visible in the viewport.
#[serde(default = "default_guides_visible")]
pub guides_visible: bool,
/// ID of the currently hovered guide for visual feedback.
#[serde(skip)]
pub hovered_guide_id: Option<GuideId>,
/// Whether or not the editor has executed the network to render the document yet. If this is opened as an inactive tab, it won't be loaded initially because the active tab is prioritized.
#[serde(skip)]
pub is_loaded: bool,
Expand Down Expand Up @@ -179,6 +192,10 @@ impl Default for DocumentMessageHandler {
saved_hash: None,
auto_saved_hash: None,
layer_range_selection_reference: None,
horizontal_guides: Vec::new(),
vertical_guides: Vec::new(),
guides_visible: true,
hovered_guide_id: None,
is_loaded: false,
}
}
Expand Down Expand Up @@ -619,6 +636,85 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
self.snapping_state.grid_snapping = visible;
responses.add(OverlaysMessage::Draw);
}
// Guide messages
DocumentMessage::CreateGuide { id, direction, position } => {
// Calculates document-to-viewport transform with offset
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);

let document_position = match direction {
super::utility_types::guide::GuideDirection::Horizontal => {
// Solve: matrix.y_axis.y * doc_y + translation.y = viewport_y
let scale_y = document_to_viewport.matrix2.y_axis.y;
if scale_y.abs() > f64::EPSILON {
(position - document_to_viewport.translation.y) / scale_y
} else {
0.0
}
}
super::utility_types::guide::GuideDirection::Vertical => {
let scale_x = document_to_viewport.matrix2.x_axis.x;
if scale_x.abs() > f64::EPSILON {
(position - document_to_viewport.translation.x) / scale_x
} else {
0.0
}
}
};

let guide = Guide::with_id(id, direction, document_position);
match direction {
super::utility_types::guide::GuideDirection::Horizontal => self.horizontal_guides.push(guide),
super::utility_types::guide::GuideDirection::Vertical => self.vertical_guides.push(guide),
}
responses.add(OverlaysMessage::Draw);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
DocumentMessage::MoveGuide { id, position } => {
// Calculate the correct document-to-viewport transform with offset
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);

// Search in both guide lists and update the position
// Use the same formula as CreateGuide to solve for document position
if let Some(guide) = self.horizontal_guides.iter_mut().find(|guide| guide.id == id) {
// Solve: matrix.y_axis.y * doc_y + translation.y = viewport_y
let scale_y = document_to_viewport.matrix2.y_axis.y;
if scale_y.abs() > f64::EPSILON {
guide.position = (position - document_to_viewport.translation.y) / scale_y;
}
} else if let Some(guide) = self.vertical_guides.iter_mut().find(|guide| guide.id == id) {
// Solve: matrix.x_axis.x * doc_x + translation.x = viewport_x
let scale_x = document_to_viewport.matrix2.x_axis.x;
if scale_x.abs() > f64::EPSILON {
guide.position = (position - document_to_viewport.translation.x) / scale_x;
}
}
responses.add(OverlaysMessage::Draw);
}
DocumentMessage::DeleteGuide { id } => {
// Remove from horizontal guides
self.horizontal_guides.retain(|g| g.id != id);
// Remove from vertical guides
self.vertical_guides.retain(|g| g.id != id);
responses.add(OverlaysMessage::Draw);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
DocumentMessage::GuideOverlays { context: mut overlay_context } => {
if self.guides_visible {
super::overlays::guide_overlays::guide_overlay(self, &mut overlay_context);
}
}
DocumentMessage::ToggleGuidesVisibility => {
self.guides_visible = !self.guides_visible;
responses.add(OverlaysMessage::Draw);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
responses.add(MenuBarMessage::SendLayout);
}
DocumentMessage::SetHoveredGuide { id } => {
if self.hovered_guide_id != id {
self.hovered_guide_id = id;
responses.add(OverlaysMessage::Draw);
}
}
DocumentMessage::GroupSelectedLayers { group_folder_type } => {
responses.add(DocumentMessage::AddTransaction);

Expand Down Expand Up @@ -2991,6 +3087,10 @@ fn default_document_network_interface() -> NodeNetworkInterface {
network_interface
}

fn default_guides_visible() -> bool {
true
}

/// Targets for the [`ClickXRayIter`]. In order to reduce computation, we prefer just a point/path test where possible.
#[derive(Clone)]
enum XRayTarget {
Expand Down
34 changes: 34 additions & 0 deletions editor/src/messages/portfolio/document/overlays/guide_overlays.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::prelude::DocumentMessageHandler;
use glam::DVec2;

const GUIDE_COLOR: &str = "#00BFFF";
const GUIDE_HOVER_COLOR: &str = "#FF6600";

pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
let document_to_viewport = document
.navigation_handler
.calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz);

let viewport_size: DVec2 = overlay_context.viewport.size().into();

for guide in &document.horizontal_guides {
let guide_point_viewport = document_to_viewport.transform_point2(DVec2::new(0.0, guide.position));
let viewport_y = guide_point_viewport.y;

let start = DVec2::new(0.0, viewport_y);
let end = DVec2::new(viewport_size.x, viewport_y);
let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR };
overlay_context.line(start, end, Some(color), None);
}

for guide in &document.vertical_guides {
let guide_point_viewport = document_to_viewport.transform_point2(DVec2::new(guide.position, 0.0));
let viewport_x = guide_point_viewport.x;

let start = DVec2::new(viewport_x, 0.0);
let end = DVec2::new(viewport_x, viewport_size.y);
let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR };
overlay_context.line(start, end, Some(color), None);
}
}
1 change: 1 addition & 0 deletions editor/src/messages/portfolio/document/overlays/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod grid_overlays;
pub mod guide_overlays;
mod overlays_message;
mod overlays_message_handler;
pub mod utility_functions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
viewport: *viewport,
},
});
responses.add(DocumentMessage::GuideOverlays {
context: OverlayContext {
render_context: canvas_context.clone(),
visibility_settings: visibility_settings.clone(),
viewport: *viewport,
},
});
for provider in &self.overlay_providers {
responses.add(provider(OverlayContext {
render_context: canvas_context.clone(),
Expand All @@ -74,6 +81,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes

if visibility_settings.all() {
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
responses.add(DocumentMessage::GuideOverlays { context: overlay_context.clone() });

for provider in &self.overlay_providers {
responses.add(provider(overlay_context.clone()));
Expand Down
61 changes: 61 additions & 0 deletions editor/src/messages/portfolio/document/utility_types/guide.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::application::generate_uuid;

#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub struct GuideId(u64);

impl GuideId {
pub fn new() -> Self {
Self(generate_uuid())
}

pub fn from_raw(id: u64) -> Self {
Self(id)
}

pub fn as_raw(&self) -> u64 {
self.0
}
}

impl Default for GuideId {
fn default() -> Self {
Self::new()
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum GuideDirection {
Horizontal,
Vertical,
}

#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Guide {
pub id: GuideId,
pub direction: GuideDirection,
/// Position in document space (Y coordinate for horizontal guides, X coordinate for vertical guides)
pub position: f64,
}

impl Guide {
pub fn new(direction: GuideDirection, position: f64) -> Self {
Self {
id: GuideId::new(),
direction,
position,
}
}

pub fn with_id(id: GuideId, direction: GuideDirection, position: f64) -> Self {
Self { id, direction, position }
}

pub fn horizontal(y: f64) -> Self {
Self::new(GuideDirection::Horizontal, y)
}

pub fn vertical(x: f64) -> Self {
Self::new(GuideDirection::Vertical, x)
}
}
22 changes: 22 additions & 0 deletions editor/src/messages/portfolio/document/utility_types/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub enum AlignAggregate {
pub struct SnappingState {
pub snapping_enabled: bool,
pub grid_snapping: bool,
pub guides: bool,
pub artboards: bool,
pub tolerance: f64,
pub bounding_box: BoundingBoxSnapping,
Expand All @@ -72,6 +73,7 @@ impl Default for SnappingState {
Self {
snapping_enabled: true,
grid_snapping: false,
guides: true,
artboards: true,
tolerance: 8.,
bounding_box: BoundingBoxSnapping::default(),
Expand Down Expand Up @@ -103,6 +105,7 @@ impl SnappingState {
},
SnapTarget::Artboard(_) => self.artboards,
SnapTarget::Grid(_) => self.grid_snapping,
SnapTarget::Guide(_) => self.guides,
SnapTarget::Alignment(AlignmentSnapTarget::AlignWithAnchorPoint) => self.path.align_with_anchor_point,
SnapTarget::Alignment(_) => self.bounding_box.align_with_edges,
SnapTarget::DistributeEvenly(_) => self.bounding_box.distribute_evenly,
Expand Down Expand Up @@ -531,6 +534,23 @@ impl fmt::Display for GridSnapTarget {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GuideSnapTarget {
Horizontal,
Vertical,
Intersection,
}

impl fmt::Display for GuideSnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GuideSnapTarget::Horizontal => write!(f, "Guide: Horizontal"),
GuideSnapTarget::Vertical => write!(f, "Guide: Vertical"),
GuideSnapTarget::Intersection => write!(f, "Guide: Intersection"),
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlignmentSnapTarget {
BoundingBoxCornerPoint,
Expand Down Expand Up @@ -598,6 +618,7 @@ pub enum SnapTarget {
Path(PathSnapTarget),
Artboard(ArtboardSnapTarget),
Grid(GridSnapTarget),
Guide(GuideSnapTarget),
Alignment(AlignmentSnapTarget),
DistributeEvenly(DistributionSnapTarget),
}
Expand All @@ -619,6 +640,7 @@ impl fmt::Display for SnapTarget {
SnapTarget::Path(path_snap_target) => write!(f, "{path_snap_target}"),
SnapTarget::Artboard(artboard_snap_target) => write!(f, "{artboard_snap_target}"),
SnapTarget::Grid(grid_snap_target) => write!(f, "{grid_snap_target}"),
SnapTarget::Guide(guide_snap_target) => write!(f, "{guide_snap_target}"),
SnapTarget::Alignment(alignment_snap_target) => write!(f, "{alignment_snap_target}"),
SnapTarget::DistributeEvenly(distribution_snap_target) => write!(f, "{distribution_snap_target}"),
}
Expand Down
Loading
Loading