mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Add an editor preference for touched/enclosed/directional based selection (#2156)
* implemented left selection logic * added logic for right ward selection * removed the logs code * corrected capitalization error * corrected capitalization error * added radio buttons for selection_mode * fixed multiple selection of checkboxes * adapted to the RadioEntryData * State management bug * integrated message system to selection_mode * updated * updated * added selection mode to transition arms * removed from portfolio message and added preference in ToolMessageData * removed dead code of selection_mode from frontend logic * removed dead code for zoomWithScroll * Cleanup * Rename, simplify, use dashed box, and highlight only outlines of layers that'll get selected * More code review --------- Co-authored-by: Pratik Agrawal <patrik@Pratiks-MacBook-Air.local> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
93880abc4c
commit
96c57605b7
17 changed files with 266 additions and 113 deletions
|
@ -221,20 +221,21 @@ impl Dispatcher {
|
|||
}
|
||||
Message::Tool(message) => {
|
||||
let document_id = self.message_handlers.portfolio_message_handler.active_document_id().unwrap();
|
||||
if let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) {
|
||||
let data = ToolMessageData {
|
||||
document_id,
|
||||
document,
|
||||
input: &self.message_handlers.input_preprocessor_message_handler,
|
||||
persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data,
|
||||
node_graph: &self.message_handlers.portfolio_message_handler.executor,
|
||||
preferences: &self.message_handlers.preferences_message_handler,
|
||||
};
|
||||
|
||||
self.message_handlers.tool_message_handler.process_message(message, &mut queue, data);
|
||||
} else {
|
||||
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
|
||||
warn!("Called ToolMessage without an active document.\nGot {message:?}");
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
let data = ToolMessageData {
|
||||
document_id,
|
||||
document,
|
||||
input: &self.message_handlers.input_preprocessor_message_handler,
|
||||
persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data,
|
||||
node_graph: &self.message_handlers.portfolio_message_handler.executor,
|
||||
preferences: &self.message_handlers.preferences_message_handler,
|
||||
};
|
||||
|
||||
self.message_handlers.tool_message_handler.process_message(message, &mut queue, data);
|
||||
}
|
||||
Message::Workspace(message) => {
|
||||
self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ());
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
pub struct PreferencesDialogMessageData<'a> {
|
||||
|
@ -31,6 +32,39 @@ impl PreferencesDialogMessageHandler {
|
|||
const TITLE: &'static str = "Editor Preferences";
|
||||
|
||||
fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout {
|
||||
let selection_section = vec![TextLabel::new("Selection").italic(true).widget_holder()];
|
||||
let selection_mode = RadioInput::new(vec![
|
||||
RadioEntryData::new(SelectionMode::Touched.to_string())
|
||||
.label(SelectionMode::Touched.to_string())
|
||||
.tooltip(SelectionMode::Touched.tooltip_description())
|
||||
.on_update(move |_| {
|
||||
PreferencesMessage::SelectionMode {
|
||||
selection_mode: SelectionMode::Touched,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new(SelectionMode::Enclosed.to_string())
|
||||
.label(SelectionMode::Enclosed.to_string())
|
||||
.tooltip(SelectionMode::Enclosed.tooltip_description())
|
||||
.on_update(move |_| {
|
||||
PreferencesMessage::SelectionMode {
|
||||
selection_mode: SelectionMode::Enclosed,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new(SelectionMode::Directional.to_string())
|
||||
.label(SelectionMode::Directional.to_string())
|
||||
.tooltip(SelectionMode::Directional.tooltip_description())
|
||||
.on_update(move |_| {
|
||||
PreferencesMessage::SelectionMode {
|
||||
selection_mode: SelectionMode::Directional,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
])
|
||||
.selected_index(Some(preferences.selection_mode as u32))
|
||||
.widget_holder();
|
||||
|
||||
let zoom_with_scroll_tooltip = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)";
|
||||
let input_section = vec![TextLabel::new("Input").italic(true).widget_holder()];
|
||||
let zoom_with_scroll = vec![
|
||||
|
@ -43,9 +77,9 @@ impl PreferencesDialogMessageHandler {
|
|||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(),
|
||||
];
|
||||
|
||||
let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)";
|
||||
let renderer_section = vec![TextLabel::new("Experimental").italic(true).widget_holder()];
|
||||
let use_vello = vec![
|
||||
|
@ -54,7 +88,6 @@ impl PreferencesDialogMessageHandler {
|
|||
.disabled(!preferences.supports_wgpu())
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextLabel::new("Vello Renderer")
|
||||
.table_align(true)
|
||||
.tooltip(vello_tooltip)
|
||||
|
@ -62,11 +95,19 @@ impl PreferencesDialogMessageHandler {
|
|||
.widget_holder(),
|
||||
];
|
||||
|
||||
let vector_mesh_tooltip = "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle line joins and fills.";
|
||||
let vector_meshes = vec![
|
||||
CheckboxInput::new(preferences.vector_meshes)
|
||||
.tooltip(vector_mesh_tooltip)
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).widget_holder(),
|
||||
];
|
||||
|
||||
// TODO: Reenable when Imaginate is restored
|
||||
// let imaginate_server_hostname = vec![
|
||||
// TextLabel::new("Imaginate").min_width(60).italic(true).widget_holder(),
|
||||
// TextLabel::new("Server Hostname").table_align(true).widget_holder(),
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// TextInput::new(&preferences.imaginate_server_hostname)
|
||||
// .min_width(200)
|
||||
// .on_update(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into())
|
||||
|
@ -75,7 +116,6 @@ impl PreferencesDialogMessageHandler {
|
|||
// let imaginate_refresh_frequency = vec![
|
||||
// TextLabel::new("").min_width(60).widget_holder(),
|
||||
// TextLabel::new("Refresh Frequency").table_align(true).widget_holder(),
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// NumberInput::new(Some(preferences.imaginate_refresh_frequency))
|
||||
// .unit(" seconds")
|
||||
// .min(0.)
|
||||
|
@ -85,17 +125,9 @@ impl PreferencesDialogMessageHandler {
|
|||
// .widget_holder(),
|
||||
// ];
|
||||
|
||||
let vector_mesh_tooltip = "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle line joins and fills.";
|
||||
let vector_meshes = vec![
|
||||
CheckboxInput::new(preferences.vector_meshes)
|
||||
.tooltip(vector_mesh_tooltip)
|
||||
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).widget_holder(),
|
||||
];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row { widgets: selection_section },
|
||||
LayoutGroup::Row { widgets: vec![selection_mode] },
|
||||
LayoutGroup::Row { widgets: input_section },
|
||||
LayoutGroup::Row { widgets: zoom_with_scroll },
|
||||
LayoutGroup::Row { widgets: renderer_section },
|
||||
|
|
|
@ -1407,6 +1407,29 @@ impl DocumentMessageHandler {
|
|||
self.intersect_quad(viewport_quad, ipp).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
}
|
||||
|
||||
pub fn is_layer_fully_inside(&self, layer: &LayerNodeIdentifier, quad: graphene_core::renderer::Quad) -> bool {
|
||||
// Get the bounding box of the layer in document space
|
||||
let Some(bounding_box) = self.metadata().bounding_box_viewport(*layer) else { return false };
|
||||
|
||||
// Check if the bounding box is fully within the selection quad
|
||||
let [top_left, bottom_right] = bounding_box;
|
||||
|
||||
let quad_bbox = quad.bounding_box();
|
||||
|
||||
let quad_left = quad_bbox[0].x;
|
||||
let quad_right = quad_bbox[1].x;
|
||||
let quad_top = quad_bbox[0].y.max(quad_bbox[1].y); // Correct top
|
||||
let quad_bottom = quad_bbox[0].y.min(quad_bbox[1].y); // Correct bottom
|
||||
|
||||
// Extract layer's bounding box coordinates
|
||||
let layer_left = top_left.x;
|
||||
let layer_right = bottom_right.x;
|
||||
let layer_top = bottom_right.y;
|
||||
let layer_bottom = top_left.y;
|
||||
|
||||
layer_left >= quad_left && layer_right <= quad_right && layer_top <= quad_top && layer_bottom >= quad_bottom
|
||||
}
|
||||
|
||||
/// Find all of the layers that were clicked on from a viewport space location
|
||||
pub fn click_xray(&self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
||||
let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
|
||||
|
|
|
@ -460,7 +460,7 @@ impl DoubleEndedIterator for DescendantsIter<'_> {
|
|||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct NodeRelations {
|
||||
parent: Option<LayerNodeIdentifier>,
|
||||
pub parent: Option<LayerNodeIdentifier>,
|
||||
previous_sibling: Option<LayerNodeIdentifier>,
|
||||
next_sibling: Option<LayerNodeIdentifier>,
|
||||
first_child: Option<LayerNodeIdentifier>,
|
||||
|
|
|
@ -46,16 +46,16 @@ pub enum PortfolioMessage {
|
|||
document_id: DocumentId,
|
||||
},
|
||||
DestroyAllDocuments,
|
||||
EditorPreferences,
|
||||
FontLoaded {
|
||||
font_family: String,
|
||||
font_style: String,
|
||||
preview_url: String,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
ImaginateCheckServerStatus,
|
||||
ImaginatePollServerStatus,
|
||||
EditorPreferences,
|
||||
ImaginateServerHostname,
|
||||
// ImaginateCheckServerStatus,
|
||||
// ImaginatePollServerStatus,
|
||||
// ImaginateServerHostname,
|
||||
Import,
|
||||
LoadDocumentResources {
|
||||
document_id: DocumentId,
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
|||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
|
||||
use crate::messages::portfolio::document::DocumentMessageData;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
|
||||
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
||||
|
@ -38,6 +39,7 @@ pub struct PortfolioMessageHandler {
|
|||
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
|
||||
pub persistent_data: PersistentData,
|
||||
pub executor: NodeGraphExecutor,
|
||||
pub selection_mode: SelectionMode,
|
||||
}
|
||||
|
||||
impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMessageHandler {
|
||||
|
@ -295,35 +297,35 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::ImaginateCheckServerStatus => {
|
||||
let server_status = self.persistent_data.imaginate.server_status().clone();
|
||||
self.persistent_data.imaginate.poll_server_check();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let () = fut.await;
|
||||
use wasm_bindgen::prelude::*;
|
||||
// PortfolioMessage::ImaginateCheckServerStatus => {
|
||||
// let server_status = self.persistent_data.imaginate.server_status().clone();
|
||||
// self.persistent_data.imaginate.poll_server_check();
|
||||
// #[cfg(target_arch = "wasm32")]
|
||||
// if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() {
|
||||
// wasm_bindgen_futures::spawn_local(async move {
|
||||
// let () = fut.await;
|
||||
// use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen(module = "/../frontend/src/editor.ts")]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
||||
fn inject();
|
||||
}
|
||||
inject();
|
||||
})
|
||||
}
|
||||
if &server_status != self.persistent_data.imaginate.server_status() {
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::ImaginatePollServerStatus => {
|
||||
self.persistent_data.imaginate.poll_server_check();
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
// #[wasm_bindgen(module = "/../frontend/src/editor.ts")]
|
||||
// extern "C" {
|
||||
// #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
||||
// fn inject();
|
||||
// }
|
||||
// inject();
|
||||
// })
|
||||
// }
|
||||
// if &server_status != self.persistent_data.imaginate.server_status() {
|
||||
// responses.add(PropertiesPanelMessage::Refresh);
|
||||
// }
|
||||
// }
|
||||
// PortfolioMessage::ImaginatePollServerStatus => {
|
||||
// self.persistent_data.imaginate.poll_server_check();
|
||||
// responses.add(PropertiesPanelMessage::Refresh);
|
||||
// }
|
||||
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
|
||||
PortfolioMessage::ImaginateServerHostname => {
|
||||
self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
|
||||
}
|
||||
// PortfolioMessage::ImaginateServerHostname => {
|
||||
// self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
|
||||
// }
|
||||
PortfolioMessage::Import => {
|
||||
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
|
||||
responses.add(FrontendMessage::TriggerImport);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use graphene_std::{imaginate::ImaginatePersistentData, text::FontCache};
|
||||
use graphene_std::imaginate::ImaginatePersistentData;
|
||||
use graphene_std::text::FontCache;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PersistentData {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
mod preferences_message;
|
||||
mod preferences_message_handler;
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use preferences_message::{PreferencesMessage, PreferencesMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use preferences_message_handler::PreferencesMessageHandler;
|
||||
#[doc(inline)]
|
||||
pub use utility_types::SelectionMode;
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, Preferences)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PreferencesMessage {
|
||||
// Management messages
|
||||
Load { preferences: String },
|
||||
ResetToDefaults,
|
||||
|
||||
ImaginateRefreshFrequency { seconds: f64 },
|
||||
// Per-preference messages
|
||||
UseVello { use_vello: bool },
|
||||
ImaginateServerHostname { hostname: String },
|
||||
SelectionMode { selection_mode: SelectionMode },
|
||||
VectorMeshes { enabled: bool },
|
||||
ModifyLayout { zoom_with_scroll: bool },
|
||||
// ImaginateRefreshFrequency { seconds: f64 },
|
||||
// ImaginateServerHostname { hostname: String },
|
||||
}
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct PreferencesMessageHandler {
|
||||
pub imaginate_server_hostname: String,
|
||||
pub imaginate_refresh_frequency: f64,
|
||||
pub selection_mode: SelectionMode,
|
||||
pub zoom_with_scroll: bool,
|
||||
pub use_vello: bool,
|
||||
pub vector_meshes: bool,
|
||||
}
|
||||
|
||||
impl PreferencesMessageHandler {
|
||||
pub fn get_selection_mode(&self) -> SelectionMode {
|
||||
self.selection_mode
|
||||
}
|
||||
|
||||
pub fn editor_preferences(&self) -> EditorPreferences {
|
||||
EditorPreferences {
|
||||
imaginate_hostname: self.imaginate_server_hostname.clone(),
|
||||
|
@ -33,6 +40,7 @@ impl Default for PreferencesMessageHandler {
|
|||
Self {
|
||||
imaginate_server_hostname: host_name,
|
||||
imaginate_refresh_frequency: 1.,
|
||||
selection_mode: SelectionMode::Touched,
|
||||
zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll),
|
||||
use_vello,
|
||||
vector_meshes: false,
|
||||
|
@ -43,6 +51,7 @@ impl Default for PreferencesMessageHandler {
|
|||
impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
||||
fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque<Message>, _data: ()) {
|
||||
match message {
|
||||
// Management messages
|
||||
PreferencesMessage::Load { preferences } => {
|
||||
if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
|
||||
*self = deserialized_preferences;
|
||||
|
@ -65,31 +74,12 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
|||
*self = Self::default()
|
||||
}
|
||||
|
||||
PreferencesMessage::ImaginateRefreshFrequency { seconds } => {
|
||||
self.imaginate_refresh_frequency = seconds;
|
||||
responses.add(PortfolioMessage::ImaginateCheckServerStatus);
|
||||
responses.add(PortfolioMessage::EditorPreferences);
|
||||
}
|
||||
// Per-preference messages
|
||||
PreferencesMessage::UseVello { use_vello } => {
|
||||
self.use_vello = use_vello;
|
||||
responses.add(PortfolioMessage::UpdateVelloPreference);
|
||||
responses.add(PortfolioMessage::EditorPreferences);
|
||||
}
|
||||
PreferencesMessage::ImaginateServerHostname { hostname } => {
|
||||
let initial = hostname.clone();
|
||||
let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://");
|
||||
let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname };
|
||||
let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" };
|
||||
|
||||
if hostname != initial {
|
||||
refresh_dialog(responses);
|
||||
}
|
||||
|
||||
self.imaginate_server_hostname = hostname;
|
||||
responses.add(PortfolioMessage::ImaginateServerHostname);
|
||||
responses.add(PortfolioMessage::ImaginateCheckServerStatus);
|
||||
responses.add(PortfolioMessage::EditorPreferences);
|
||||
}
|
||||
PreferencesMessage::VectorMeshes { enabled } => {
|
||||
self.vector_meshes = enabled;
|
||||
}
|
||||
|
@ -102,7 +92,31 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
|||
};
|
||||
responses.add(KeyMappingMessage::ModifyMapping(variant));
|
||||
}
|
||||
PreferencesMessage::SelectionMode { selection_mode } => {
|
||||
self.selection_mode = selection_mode;
|
||||
}
|
||||
}
|
||||
// TODO: Reenable when Imaginate is restored (and move back up one line since the auto-formatter doesn't like it in that block)
|
||||
// PreferencesMessage::ImaginateRefreshFrequency { seconds } => {
|
||||
// self.imaginate_refresh_frequency = seconds;
|
||||
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
|
||||
// responses.add(PortfolioMessage::EditorPreferences);
|
||||
// }
|
||||
// PreferencesMessage::ImaginateServerHostname { hostname } => {
|
||||
// let initial = hostname.clone();
|
||||
// let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://");
|
||||
// let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname };
|
||||
// let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" };
|
||||
|
||||
// if hostname != initial {
|
||||
// refresh_dialog(responses);
|
||||
// }
|
||||
|
||||
// self.imaginate_server_hostname = hostname;
|
||||
// responses.add(PortfolioMessage::ImaginateServerHostname);
|
||||
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
|
||||
// responses.add(PortfolioMessage::EditorPreferences);
|
||||
//}
|
||||
|
||||
responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() });
|
||||
}
|
||||
|
|
27
editor/src/messages/preferences/utility_types.rs
Normal file
27
editor/src/messages/preferences/utility_types.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type, Hash)]
|
||||
pub enum SelectionMode {
|
||||
#[default]
|
||||
Touched = 0,
|
||||
Enclosed = 1,
|
||||
Directional = 2,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SelectionMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SelectionMode::Touched => write!(f, "Touched"),
|
||||
SelectionMode::Enclosed => write!(f, "Enclosed"),
|
||||
SelectionMode::Directional => write!(f, "Directional"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionMode {
|
||||
pub fn tooltip_description(&self) -> &'static str {
|
||||
match self {
|
||||
SelectionMode::Touched => "Select all layers at least partially covered by the dragged selection area",
|
||||
SelectionMode::Enclosed => "Select only layers fully enclosed by the dragged selection area",
|
||||
SelectionMode::Directional => r#""Touched" for leftward drags, "Enclosed" for rightward drags"#,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,11 +21,11 @@ pub struct SizeSnapData<'a> {
|
|||
/// Contains the edges that are being dragged along with the original bounds.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SelectedEdges {
|
||||
bounds: [DVec2; 2],
|
||||
top: bool,
|
||||
bottom: bool,
|
||||
left: bool,
|
||||
right: bool,
|
||||
pub bounds: [DVec2; 2],
|
||||
pub top: bool,
|
||||
pub bottom: bool,
|
||||
pub left: bool,
|
||||
pub right: bool,
|
||||
// Aspect ratio in the form of width/height, so x:1 = width:height
|
||||
aspect_ratio: f64,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::utility_types::ToolType;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene_core::raster::color::Color;
|
||||
|
@ -98,4 +99,7 @@ pub enum ToolMessage {
|
|||
Undo,
|
||||
UpdateCursor,
|
||||
UpdateHints,
|
||||
UpdateSelectionMode {
|
||||
selection_mode: SelectionMode,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate};
|
||||
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_text, is_layer_fed_by_node_of_name};
|
||||
use crate::messages::tool::common_functionality::pivot::Pivot;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
|
||||
|
@ -250,12 +251,13 @@ impl ToolTransition for SelectTool {
|
|||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
enum SelectToolFsmState {
|
||||
Ready { selection: NestedSelectionBehavior },
|
||||
DrawingBox { selection: NestedSelectionBehavior },
|
||||
DrawingBox,
|
||||
Dragging,
|
||||
ResizingBounds,
|
||||
RotatingBounds,
|
||||
DraggingPivot,
|
||||
}
|
||||
|
||||
impl Default for SelectToolFsmState {
|
||||
fn default() -> Self {
|
||||
let selection = NestedSelectionBehavior::Deepest;
|
||||
|
@ -297,12 +299,22 @@ impl SelectToolData {
|
|||
}
|
||||
}
|
||||
|
||||
fn selection_quad(&self) -> Quad {
|
||||
pub fn selection_quad(&self) -> Quad {
|
||||
let bbox = self.selection_box();
|
||||
Quad::from_box(bbox)
|
||||
}
|
||||
|
||||
fn selection_box(&self) -> [DVec2; 2] {
|
||||
pub fn calculate_direction(&self) -> SelectionMode {
|
||||
let bbox: [DVec2; 2] = self.selection_box();
|
||||
if bbox[1].x < bbox[0].x {
|
||||
SelectionMode::Touched
|
||||
} else {
|
||||
// This also covers the case where they're equal: the area is zero, so we use `Enclosed` to ensure the selection ends up empty, as nothing will be enclosed by an empty area
|
||||
SelectionMode::Enclosed
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selection_box(&self) -> [DVec2; 2] {
|
||||
if self.drag_current == self.drag_start {
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
[self.drag_start - tolerance, self.drag_start + tolerance]
|
||||
|
@ -475,21 +487,41 @@ impl Fsm for SelectToolFsmState {
|
|||
tool_data.pivot.update_pivot(document, &mut overlay_context);
|
||||
|
||||
// Check if the tool is in box selection mode
|
||||
if matches!(self, Self::DrawingBox { .. }) {
|
||||
if matches!(self, Self::DrawingBox) {
|
||||
// Get the updated selection box bounds
|
||||
let quad = Quad::from_box([tool_data.drag_start, tool_data.drag_current]);
|
||||
|
||||
let mut selection_direction = tool_action_data.preferences.get_selection_mode();
|
||||
if selection_direction == SelectionMode::Directional {
|
||||
selection_direction = tool_data.calculate_direction();
|
||||
}
|
||||
|
||||
// Draw outline visualizations on the layers to be selected
|
||||
for layer in document.intersect_quad_no_artboards(quad, input) {
|
||||
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
|
||||
let mut draw_layer_outline = |layer| overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
|
||||
let intersection = document.intersect_quad_no_artboards(quad, input);
|
||||
if selection_direction == SelectionMode::Enclosed {
|
||||
for layer in intersection.filter(|layer| document.is_layer_fully_inside(layer, quad)) {
|
||||
draw_layer_outline(layer);
|
||||
}
|
||||
} else {
|
||||
for layer in intersection {
|
||||
draw_layer_outline(layer);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the selection box
|
||||
let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.05)
|
||||
.rgba_hex();
|
||||
overlay_context.quad(quad, Some(&("#".to_string() + &fill_color)));
|
||||
fill_color.insert(0, '#');
|
||||
let fill_color = Some(fill_color.as_str());
|
||||
|
||||
if selection_direction == SelectionMode::Enclosed {
|
||||
overlay_context.dashed_quad(quad, fill_color, Some(4.), Some(4.), Some(0.5));
|
||||
} else {
|
||||
overlay_context.quad(quad, fill_color);
|
||||
}
|
||||
}
|
||||
// Only highlight layers if the viewport is not being panned (middle mouse button is pressed)
|
||||
// TODO: Don't use `Key::Mmb` directly, instead take it as a variable from the input mappings list like in all other places
|
||||
|
@ -675,9 +707,7 @@ impl Fsm for SelectToolFsmState {
|
|||
responses.add(DocumentMessage::StartTransaction);
|
||||
SelectToolFsmState::Dragging
|
||||
} else {
|
||||
// Make a box selection, preserving previously selected layers
|
||||
let selection = tool_data.nested_selection_behavior;
|
||||
SelectToolFsmState::DrawingBox { selection }
|
||||
SelectToolFsmState::DrawingBox
|
||||
}
|
||||
};
|
||||
tool_data.non_duplicated_layers = None;
|
||||
|
@ -824,7 +854,7 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
SelectToolFsmState::DraggingPivot
|
||||
}
|
||||
(SelectToolFsmState::DrawingBox { .. }, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
tool_data.drag_current = input.mouse.position;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
|
@ -835,8 +865,7 @@ impl Fsm for SelectToolFsmState {
|
|||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
let selection = tool_data.nested_selection_behavior;
|
||||
SelectToolFsmState::DrawingBox { selection }
|
||||
SelectToolFsmState::DrawingBox
|
||||
}
|
||||
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => {
|
||||
let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
|
||||
|
@ -883,7 +912,7 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
self
|
||||
}
|
||||
(SelectToolFsmState::DrawingBox { .. }, SelectToolMessage::PointerOutsideViewport(_)) => {
|
||||
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerOutsideViewport(_)) => {
|
||||
// AutoPanning
|
||||
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
|
||||
tool_data.drag_start += shift;
|
||||
|
@ -1012,14 +1041,26 @@ impl Fsm for SelectToolFsmState {
|
|||
SelectToolFsmState::Ready { selection }
|
||||
}
|
||||
(
|
||||
SelectToolFsmState::DrawingBox { .. },
|
||||
SelectToolFsmState::DrawingBox,
|
||||
SelectToolMessage::DragStop {
|
||||
remove_from_selection,
|
||||
negative_box_selection,
|
||||
},
|
||||
) => {
|
||||
let quad = tool_data.selection_quad();
|
||||
let new_selected: HashSet<_> = document.intersect_quad_no_artboards(quad, input).collect();
|
||||
|
||||
let mut selection_direction = tool_action_data.preferences.get_selection_mode();
|
||||
if selection_direction == SelectionMode::Directional {
|
||||
selection_direction = tool_data.calculate_direction();
|
||||
}
|
||||
|
||||
let intersection = document.intersect_quad_no_artboards(quad, input);
|
||||
let new_selected: HashSet<_> = if selection_direction == SelectionMode::Enclosed {
|
||||
intersection.filter(|layer| document.is_layer_fully_inside(layer, quad)).collect()
|
||||
} else {
|
||||
intersection.collect()
|
||||
};
|
||||
|
||||
let current_selected: HashSet<_> = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect();
|
||||
if new_selected != current_selected {
|
||||
// Negative selection when both Shift and Ctrl are pressed
|
||||
|
@ -1166,7 +1207,7 @@ impl Fsm for SelectToolFsmState {
|
|||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
SelectToolFsmState::DrawingBox { .. } => {
|
||||
SelectToolFsmState::DrawingBox => {
|
||||
let hint_data = HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Control, Key::Shift], "Remove from Selection").add_mac_keys([Key::Command, Key::Shift])]),
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::messages::input_mapper::utility_types::macros::action_keys;
|
|||
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
||||
use crate::messages::preferences::PreferencesMessageHandler;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ export function createEditor(): Editor {
|
|||
// TODO: Then, delete the `(window as any).editorHandle = handle;` line above.
|
||||
// This function is called by an FFI binding within the Rust code directly, rather than using the FrontendMessage system.
|
||||
// Then, this directly calls the `injectImaginatePollServerStatus` function on the `EditorHandle` object which is a JS binding generated by wasm-bindgen, going straight back into the Rust code.
|
||||
export function injectImaginatePollServerStatus() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).editorHandle?.injectImaginatePollServerStatus();
|
||||
}
|
||||
// export function injectImaginatePollServerStatus() {
|
||||
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// (window as any).editorHandle?.injectImaginatePollServerStatus();
|
||||
// }
|
||||
|
|
|
@ -725,10 +725,10 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
||||
pub fn inject_imaginate_poll_server_status(&self) {
|
||||
self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
|
||||
}
|
||||
// #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
||||
// pub fn inject_imaginate_poll_server_status(&self) {
|
||||
// self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
|
||||
// }
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
#[wasm_bindgen(js_name = triggerUpgradeDocumentToVectorManipulationFormat)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue