diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 8f98bfe1..1967c85d 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -240,7 +240,17 @@ pub enum FrontendMessage { #[serde(rename = "hintData")] hint_data: HintData, }, - UpdateLayersPanelControlBarLayout { + UpdateLayersPanelControlBarLeftLayout { + #[serde(rename = "layoutTarget")] + layout_target: LayoutTarget, + diff: Vec, + }, + UpdateLayersPanelControlBarRightLayout { + #[serde(rename = "layoutTarget")] + layout_target: LayoutTarget, + diff: Vec, + }, + UpdateLayersPanelBottomBarLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, diff: Vec, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index a5b33e56..9c921715 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -424,7 +424,9 @@ impl LayoutMessageHandler { LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff }, LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff }, LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff }, - LayoutTarget::LayersPanelControlBar => FrontendMessage::UpdateLayersPanelControlBarLayout { layout_target, diff }, + LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff }, + LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff }, + LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff }, LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"), LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff }, LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff }, diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index a183a89e..8b576c59 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -31,8 +31,12 @@ pub enum LayoutTarget { DocumentBar, /// Contains the dropdown for design / select / guide mode found on the top left of the canvas. DocumentMode, - /// Options for opacity seen at the top of the Layers panel. - LayersPanelControlBar, + /// Blending options at the top of the Layers panel. + LayersPanelControlLeftBar, + /// Selected layer status (locked/hidden) at the top of the Layers panel. + LayersPanelControlRightBar, + /// Controls for adding, grouping, and deleting layers at the bottom of the Layers panel. + LayersPanelBottomBar, /// The dropdown menu at the very top of the application: File, Edit, etc. MenuBar, /// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons. diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index 8025d7da..54fbc920 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -42,6 +42,9 @@ pub struct IconButton { pub struct PopoverButton { pub style: Option, + #[serde(rename = "menuDirection")] + pub menu_direction: Option, + pub icon: Option, pub disabled: bool, @@ -58,6 +61,20 @@ pub struct PopoverButton { pub popover_min_width: Option, } +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum MenuDirection { + Top, + #[default] + Bottom, + Left, + Right, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + Center, +} + #[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] #[derivative(Debug, PartialEq)] pub struct ParameterExposeButton { diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 33353dbf..fe917b11 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -67,6 +67,13 @@ pub struct DropdownInput { #[serde(skip)] pub tooltip_shortcut: Option, + + // Styling + #[serde(rename = "minWidth")] + pub min_width: u32, + + #[serde(rename = "maxWidth")] + pub max_width: u32, // // Callbacks // `on_update` exists on the `MenuListEntry`, not this parent `DropdownInput` @@ -208,6 +215,9 @@ pub struct NumberInput { #[serde(rename = "minWidth")] pub min_width: u32, + #[serde(rename = "maxWidth")] + pub max_width: u32, + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index df9adc69..b4d50dbd 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -299,7 +299,17 @@ impl MessageHandler> for DocumentMessag // Clear the control bar responses.add(LayoutMessage::SendLayout { layout: Layout::WidgetLayout(Default::default()), - layout_target: LayoutTarget::LayersPanelControlBar, + layout_target: LayoutTarget::LayersPanelControlLeftBar, + }); + responses.add(LayoutMessage::SendLayout { + layout: Layout::WidgetLayout(Default::default()), + layout_target: LayoutTarget::LayersPanelControlRightBar, + }); + + // Clear the bottom bar + responses.add(LayoutMessage::SendLayout { + layout: Layout::WidgetLayout(Default::default()), + layout_target: LayoutTarget::LayersPanelBottomBar, }); } DocumentMessage::CreateEmptyFolder => { @@ -344,6 +354,7 @@ impl MessageHandler> for DocumentMessag DocumentMessage::DocumentHistoryForward => self.redo_with_history(ipp, responses), DocumentMessage::DocumentStructureChanged => { self.update_layers_panel_control_bar_widgets(responses); + self.update_layers_panel_bottom_bar_widgets(responses); self.network_interface.load_structure(); let data_buffer: RawBuffer = self.serialize_root(); @@ -2509,12 +2520,14 @@ impl DocumentMessageHandler { .selected_index(blend_mode.and_then(|blend_mode| blend_mode.index_in_list_svg_subset()).map(|index| index as u32)) .disabled(disabled) .draw_icon(false) + .max_width(100) + .tooltip("Blend Mode") .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), NumberInput::new(opacity) .label("Opacity") .unit("%") - .display_decimal_places(2) + .display_decimal_places(0) .disabled(disabled) .min(0.) .max(100.) @@ -2529,33 +2542,13 @@ impl DocumentMessageHandler { } }) .on_commit(|_| DocumentMessage::AddTransaction.into()) + .max_width(100) + .tooltip("Opacity") .widget_holder(), - // - Separator::new(SeparatorType::Unrelated).widget_holder(), - // - IconButton::new("NewLayer", 24) - .tooltip("New Layer") - .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder)) - .on_update(|_| DocumentMessage::CreateEmptyFolder.into()) - .widget_holder(), - IconButton::new("Folder", 24) - .tooltip("Group Selected") - .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers)) - .on_update(|_| { - let group_folder_type = GroupFolderType::Layer; - DocumentMessage::GroupSelectedLayers { group_folder_type }.into() - }) - .disabled(!has_selection) - .widget_holder(), - IconButton::new("Trash", 24) - .tooltip("Delete Selected") - .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers)) - .on_update(|_| DocumentMessage::DeleteSelectedLayers.into()) - .disabled(!has_selection) - .widget_holder(), - // - Separator::new(SeparatorType::Unrelated).widget_holder(), - // + ]; + let layers_panel_control_bar_left = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]); + + let widgets = vec![ IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24) .hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into())) .tooltip(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" }) @@ -2571,11 +2564,75 @@ impl DocumentMessageHandler { .disabled(!has_selection) .widget_holder(), ]; - let layers_panel_control_bar = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]); + let layers_panel_control_bar_right = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]); responses.add(LayoutMessage::SendLayout { - layout: Layout::WidgetLayout(layers_panel_control_bar), - layout_target: LayoutTarget::LayersPanelControlBar, + layout: Layout::WidgetLayout(layers_panel_control_bar_left), + layout_target: LayoutTarget::LayersPanelControlLeftBar, + }); + responses.add(LayoutMessage::SendLayout { + layout: Layout::WidgetLayout(layers_panel_control_bar_right), + layout_target: LayoutTarget::LayersPanelControlRightBar, + }); + } + + pub fn update_layers_panel_bottom_bar_widgets(&self, responses: &mut VecDeque) { + let selected_nodes = self.network_interface.selected_nodes(); + let mut selected_layers = selected_nodes.selected_layers(self.metadata()); + let selected_layer = selected_layers.next(); + let has_selection = selected_layer.is_some(); + let has_multiple_selection = selected_layers.next().is_some(); + + let widgets = vec![ + PopoverButton::new() + .icon(Some("Node".to_string())) + .menu_direction(Some(MenuDirection::Top)) + .tooltip("Add an operation to the end of this layer's chain of nodes") + .disabled(!has_selection || has_multiple_selection) + .popover_layout({ + let node_chooser = NodeCatalog::new() + .on_update(move |node_type| { + if let Some(layer) = selected_layer { + NodeGraphMessage::CreateNodeInLayerWithTransaction { + node_type: node_type.clone(), + layer: LayerNodeIdentifier::new_unchecked(layer.to_node()), + } + .into() + } else { + Message::NoOp + } + }) + .widget_holder(); + vec![LayoutGroup::Row { widgets: vec![node_chooser] }] + }) + .widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + IconButton::new("Folder", 24) + .tooltip("Group Selected") + .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers)) + .on_update(|_| { + let group_folder_type = GroupFolderType::Layer; + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() + }) + .disabled(!has_selection) + .widget_holder(), + IconButton::new("NewLayer", 24) + .tooltip("New Layer") + .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder)) + .on_update(|_| DocumentMessage::CreateEmptyFolder.into()) + .widget_holder(), + IconButton::new("Trash", 24) + .tooltip("Delete Selected") + .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers)) + .on_update(|_| DocumentMessage::DeleteSelectedLayers.into()) + .disabled(!has_selection) + .widget_holder(), + ]; + let layers_panel_bottom_bar = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]); + + responses.add(LayoutMessage::SendLayout { + layout: Layout::WidgetLayout(layers_panel_bottom_bar), + layout_target: LayoutTarget::LayersPanelBottomBar, }); } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index e9f267e7..cb490069 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -2009,12 +2009,38 @@ impl NodeGraphMessageHandler { } // Next, we decide what to display based on the number of layers and nodes selected - match layers.len() { + match *layers.as_slice() { // If no layers are selected, show properties for all selected nodes - 0 => { + [] => { let selected_nodes = nodes.iter().map(|node_id| node_properties::generate_node_properties(*node_id, context)).collect::>(); if !selected_nodes.is_empty() { - return selected_nodes; + let mut properties = Vec::new(); + + if let [node_id] = *nodes.as_slice() { + properties.push(LayoutGroup::Row { + widgets: vec![ + Separator::new(SeparatorType::Related).widget_holder(), + IconLabel::new("Node").tooltip("Name of the selected node").widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + TextInput::new(context.network_interface.display_name(&node_id, context.selection_network_path)) + .tooltip("Name of the selected node") + .on_update(move |text_input| { + NodeGraphMessage::SetDisplayName { + node_id, + alias: text_input.value.clone(), + skip_adding_history_step: false, + } + .into() + }) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + ], + }); + } + + properties.extend(selected_nodes); + + return properties; } // TODO: Display properties for encapsulating node when no nodes are selected in a nested network @@ -2056,8 +2082,7 @@ impl NodeGraphMessageHandler { properties } // If one layer is selected, filter out all selected nodes that are not upstream of it. If there are no nodes left, show properties for the layer. Otherwise, show nothing. - 1 => { - let layer = layers[0]; + [layer] => { let nodes_not_upstream_of_layer = nodes.into_iter().filter(|&selected_node_id| { !context .network_interface diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 1b6ab693..76298f04 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1431,6 +1431,7 @@ impl PortfolioMessageHandler { self.document_ids.push_back(document_id); } new_document.update_layers_panel_control_bar_widgets(responses); + new_document.update_layers_panel_bottom_bar_widgets(responses); self.documents.insert(document_id, new_document); diff --git a/frontend/assets/icon-12px-solid/clipped.svg b/frontend/assets/icon-12px-solid/clipped.svg new file mode 100644 index 00000000..76c2e894 --- /dev/null +++ b/frontend/assets/icon-12px-solid/clipped.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index 6167897b..53a20010 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -3,10 +3,11 @@ import type { Editor } from "@graphite/editor"; import type { HSV, RGB, FillChoice } from "@graphite/messages"; + import type { MenuDirection } from "@graphite/messages"; import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages"; import { clamp } from "@graphite/utility-functions/math"; - import FloatingMenu, { type MenuDirection } from "@graphite/components/layout/FloatingMenu.svelte"; + import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte"; import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; diff --git a/frontend/src/components/floating-menus/MenuList.svelte b/frontend/src/components/floating-menus/MenuList.svelte index 1988c3b7..c2528767 100644 --- a/frontend/src/components/floating-menus/MenuList.svelte +++ b/frontend/src/components/floating-menus/MenuList.svelte @@ -3,10 +3,10 @@ - + onClick()} icon={style || "DropdownArrow"} size={16} {tooltip} data-floating-menu-spawner /> {#if icon !== undefined} {/if} - (open = detail)} minWidth={popoverMinWidth} type="Popover" direction="Bottom"> + (open = detail)} minWidth={popoverMinWidth} type="Popover" direction={menuDirection || "Bottom"}> @@ -48,6 +50,10 @@ padding-left: calc(36px - 16px); box-sizing: content-box; } + + &.direction-top .dropdown-icon .icon-label { + transform: rotate(180deg); + } } .dropdown-icon { @@ -86,5 +92,9 @@ margin-bottom: -8px; } } + + &.direction-top .floating-menu { + bottom: 100%; + } } diff --git a/frontend/src/components/widgets/buttons/TextButton.svelte b/frontend/src/components/widgets/buttons/TextButton.svelte index cf918cbd..aaac0a50 100644 --- a/frontend/src/components/widgets/buttons/TextButton.svelte +++ b/frontend/src/components/widgets/buttons/TextButton.svelte @@ -55,7 +55,7 @@ class:emphasized class:disabled class:flush - style:min-width={minWidth > 0 ? `${minWidth}px` : ""} + style:min-width={minWidth > 0 ? `${minWidth}px` : undefined} title={tooltip} data-emphasized={emphasized || undefined} data-disabled={disabled || undefined} diff --git a/frontend/src/components/widgets/inputs/DropdownInput.svelte b/frontend/src/components/widgets/inputs/DropdownInput.svelte index c976bf9c..004c119f 100644 --- a/frontend/src/components/widgets/inputs/DropdownInput.svelte +++ b/frontend/src/components/widgets/inputs/DropdownInput.svelte @@ -21,12 +21,13 @@ export let interactive = true; export let disabled = false; export let tooltip: string | undefined = undefined; + export let minWidth = 0; + export let maxWidth = 0; let activeEntry = makeActiveEntry(); let activeEntrySkipWatcher = false; let initialSelectedIndex: number | undefined = undefined; let open = false; - let minWidth = 0; $: watchSelectedIndex(selectedIndex); $: watchActiveEntry(activeEntry); @@ -76,11 +77,15 @@ } - + 0 ? { "min-width": `${minWidth}px` } : {}), ...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}) }} + bind:this={self} + data-dropdown-input +> !disabled && (open = true)} on:blur={unFocusDropdownBox} diff --git a/frontend/src/components/widgets/inputs/FontInput.svelte b/frontend/src/components/widgets/inputs/FontInput.svelte index aa72b208..dd084a91 100644 --- a/frontend/src/components/widgets/inputs/FontInput.svelte +++ b/frontend/src/components/widgets/inputs/FontInput.svelte @@ -104,7 +104,15 @@ - + 0 ? { "min-width": `${minWidth}px` } : {}) }} + {tooltip} + tabindex={disabled ? -1 : 0} + on:click={toggleOpen} + data-floating-menu-spawner + > {activeEntry?.value || ""} diff --git a/frontend/src/components/widgets/inputs/NumberInput.svelte b/frontend/src/components/widgets/inputs/NumberInput.svelte index c09758e9..82a48a49 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.svelte +++ b/frontend/src/components/widgets/inputs/NumberInput.svelte @@ -53,6 +53,7 @@ // Styling export let minWidth = 0; + export let maxWidth = 0; // Callbacks export let incrementCallbackIncrease: (() => void) | undefined = undefined; @@ -88,6 +89,7 @@ $: sliderStepValue = isInteger ? (step === undefined ? 1 : step) : "any"; $: styles = { ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}), + ...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}), ...(mode === "Range" ? { "--progress-factor": Math.min(Math.max((rangeSliderValueAsRendered - rangeMin) / (rangeMax - rangeMin), 0), 1) } : {}), }; diff --git a/frontend/src/components/widgets/inputs/RadioInput.svelte b/frontend/src/components/widgets/inputs/RadioInput.svelte index b1a03a17..91cb9533 100644 --- a/frontend/src/components/widgets/inputs/RadioInput.svelte +++ b/frontend/src/components/widgets/inputs/RadioInput.svelte @@ -24,7 +24,7 @@ } - 0 ? `${minWidth}px` : "" }}> + 0 ? { "min-width": `${minWidth}px` } : {}) }}> {#each entries as entry, index}