Implement dragging layers into the group/new/delete buttons in the Layers panel (#4153)
This commit is contained in:
parent
79df7cfa87
commit
06e2a049de
|
|
@ -26,4 +26,8 @@ pub enum LayoutMessage {
|
||||||
widget_id: WidgetId,
|
widget_id: WidgetId,
|
||||||
value: serde_json::Value,
|
value: serde_json::Value,
|
||||||
},
|
},
|
||||||
|
WidgetValueDragDrop {
|
||||||
|
layout_target: LayoutTarget,
|
||||||
|
widget_id: WidgetId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,19 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
|
||||||
LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value } => {
|
LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value } => {
|
||||||
self.handle_widget_callback(layout_target, widget_id, value, WidgetValueAction::Update, responses);
|
self.handle_widget_callback(layout_target, widget_id, value, WidgetValueAction::Update, responses);
|
||||||
}
|
}
|
||||||
|
LayoutMessage::WidgetValueDragDrop { layout_target, widget_id } => {
|
||||||
|
let Some(layout) = self.layouts.get_mut(layout_target as usize) else {
|
||||||
|
warn!("WidgetValueDragDrop referenced an invalid layout. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(widget_instance) = layout.iter_mut().find(|widget| widget.widget_id == widget_id) else {
|
||||||
|
warn!("WidgetValueDragDrop referenced an invalid widget ID. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Widget::IconButton(icon_button) = &mut *widget_instance.widget {
|
||||||
|
responses.add((icon_button.on_drag_drop.callback)(icon_button));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ pub struct IconButton {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
pub on_commit: WidgetCallback<()>,
|
pub on_commit: WidgetCallback<()>,
|
||||||
|
#[serde(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub on_drag_drop: WidgetCallback<IconButton>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
|
|
||||||
|
|
@ -3288,17 +3288,23 @@ impl DocumentMessageHandler {
|
||||||
let group_folder_type = GroupFolderType::Layer;
|
let group_folder_type = GroupFolderType::Layer;
|
||||||
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
||||||
})
|
})
|
||||||
|
.on_drag_drop(|_| {
|
||||||
|
let group_folder_type = GroupFolderType::Layer;
|
||||||
|
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
||||||
|
})
|
||||||
.disabled(!has_selection)
|
.disabled(!has_selection)
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
IconButton::new("NewLayer", 24)
|
IconButton::new("NewLayer", 24)
|
||||||
.tooltip_label("New Layer")
|
.tooltip_label("New Layer")
|
||||||
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::CreateEmptyFolder))
|
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::CreateEmptyFolder))
|
||||||
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
|
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
|
||||||
|
.on_drag_drop(|_| DocumentMessage::DuplicateSelectedLayers.into())
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
IconButton::new("Trash", 24)
|
IconButton::new("Trash", 24)
|
||||||
.tooltip_label("Delete Selected")
|
.tooltip_label("Delete Selected")
|
||||||
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::DeleteSelectedLayers))
|
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::DeleteSelectedLayers))
|
||||||
.on_update(|_| DocumentMessage::DeleteSelectedLayers.into())
|
.on_update(|_| DocumentMessage::DeleteSelectedLayers.into())
|
||||||
|
.on_drag_drop(|_| DocumentMessage::DeleteSelectedLayers.into())
|
||||||
.disabled(!has_selection)
|
.disabled(!has_selection)
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
|
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
|
||||||
let justFinishedDrag = false; // Used to prevent click events after a drag
|
let justFinishedDrag = false; // Used to prevent click events after a drag
|
||||||
let dragInPanel = false;
|
let dragInPanel = false;
|
||||||
|
let dragDropTarget: HTMLElement | undefined = undefined;
|
||||||
|
|
||||||
// Interactive clipping
|
// Interactive clipping
|
||||||
let layerToClipUponClick: LayerListingInfo | undefined = undefined;
|
let layerToClipUponClick: LayerListingInfo | undefined = undefined;
|
||||||
|
|
@ -258,7 +259,7 @@
|
||||||
|
|
||||||
let markerHeight = 0;
|
let markerHeight = 0;
|
||||||
const layerPanel = document.querySelector("[data-layer-panel]"); // Selects the element with the data-layer-panel attribute
|
const layerPanel = document.querySelector("[data-layer-panel]"); // Selects the element with the data-layer-panel attribute
|
||||||
if (layerPanel !== null && treeChildren !== undefined && treeOffset !== undefined) {
|
if (layerPanel && treeChildren && treeOffset !== undefined) {
|
||||||
let layerPanelTop = layerPanel.getBoundingClientRect().top;
|
let layerPanelTop = layerPanel.getBoundingClientRect().top;
|
||||||
Array.from(treeChildren).forEach((treeChild) => {
|
Array.from(treeChildren).forEach((treeChild) => {
|
||||||
const indexAttribute = treeChild.getAttribute("data-index");
|
const indexAttribute = treeChild.getAttribute("data-index");
|
||||||
|
|
@ -358,6 +359,18 @@
|
||||||
|
|
||||||
// Perform drag calculations if a drag is occurring
|
// Perform drag calculations if a drag is occurring
|
||||||
if (internalDragState.active) {
|
if (internalDragState.active) {
|
||||||
|
// Check if the cursor is over any element flagged as a drag drop target
|
||||||
|
// (e.g. a bottom-bar action button whose backend widget has an `on_drag_drop` callback set)
|
||||||
|
const droppable = (e.target instanceof Element && e.target.closest("[data-drag-droppable]")) || undefined;
|
||||||
|
dragDropTarget = droppable instanceof HTMLElement ? droppable : undefined;
|
||||||
|
|
||||||
|
// Hide the move-in-tree insert indicator whenever the cursor enters the bottom bar
|
||||||
|
const overBottomBar = ((e.target instanceof Element && e.target.closest("[data-layer-bottom-bar]")) || undefined) !== undefined;
|
||||||
|
if (dragDropTarget || overBottomBar) {
|
||||||
|
draggingData = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const select = () => {
|
const select = () => {
|
||||||
if (internalDragState && !$nodeGraph.selected.includes(internalDragState.layerId)) {
|
if (internalDragState && !$nodeGraph.selected.includes(internalDragState.layerId)) {
|
||||||
selectLayer(internalDragState.listing, false, false);
|
selectLayer(internalDragState.listing, false, false);
|
||||||
|
|
@ -369,7 +382,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function draggingPointerUp() {
|
function draggingPointerUp() {
|
||||||
if (internalDragState?.active && draggingData) {
|
if (internalDragState?.active && dragDropTarget) {
|
||||||
|
// Ensure the dragged layer is part of the selection, matching the move-in-tree behavior
|
||||||
|
if (!$nodeGraph.selected.includes(internalDragState.layerId)) selectLayer(internalDragState.listing, false, false);
|
||||||
|
|
||||||
|
// Hand off to the button's backend `on_drag_drop` callback via the custom event
|
||||||
|
dragDropTarget.dispatchEvent(new CustomEvent("dragdrop"));
|
||||||
|
|
||||||
|
justFinishedDrag = true;
|
||||||
|
} else if (internalDragState?.active && draggingData) {
|
||||||
const { select, insertParentId, insertIndex } = draggingData;
|
const { select, insertParentId, insertIndex } = draggingData;
|
||||||
|
|
||||||
// Commit the move
|
// Commit the move
|
||||||
|
|
@ -394,6 +415,7 @@
|
||||||
draggingData = undefined;
|
draggingData = undefined;
|
||||||
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
|
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
|
||||||
dragInPanel = false;
|
dragInPanel = false;
|
||||||
|
dragDropTarget = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function draggingMouseDown(e: MouseEvent) {
|
function draggingMouseDown(e: MouseEvent) {
|
||||||
|
|
@ -658,7 +680,7 @@
|
||||||
<div class="insert-mark" style:left={`${4 + draggingData.insertDepth * 16}px`} style:top={`${draggingData.markerHeight}px`}></div>
|
<div class="insert-mark" style:left={`${4 + draggingData.insertDepth * 16}px`} style:top={`${draggingData.markerHeight}px`}></div>
|
||||||
{/if}
|
{/if}
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="bottom-bar" scrollableX={true}>
|
<LayoutRow class="bottom-bar" classes={{ "layer-drag-active": Boolean(internalDragState?.active) }} scrollableX={true} data-layer-bottom-bar>
|
||||||
<WidgetLayout layout={$layersPanelBottomBarLayout} layoutTarget="LayersPanelBottomBar" />
|
<WidgetLayout layout={$layersPanelBottomBarLayout} layoutTarget="LayersPanelBottomBar" />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
|
|
@ -698,6 +720,26 @@
|
||||||
&:not(:has(*)) {
|
&:not(:has(*)) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.layer-drag-active .icon-button,
|
||||||
|
&.layer-drag-active .popover-button {
|
||||||
|
&.drag-droppable:hover {
|
||||||
|
background: var(--color-e-nearwhite);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--color-2-mildblack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.drag-droppable) {
|
||||||
|
pointer-events: none;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--color-8-uppergray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layer hierarchy
|
// Layer hierarchy
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@
|
||||||
editor.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
|
editor.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function widgetValueDragDrop(widgetIndex: number) {
|
||||||
|
editor.widgetValueDragDrop(layoutTarget, widgets[widgetIndex].widgetId);
|
||||||
|
}
|
||||||
|
|
||||||
// Extracts the kind and props from a Widget tagged enum, validated against the widget registry.
|
// Extracts the kind and props from a Widget tagged enum, validated against the widget registry.
|
||||||
// The overload declares the precise correlated return type while the implementation uses broader types.
|
// The overload declares the precise correlated return type while the implementation uses broader types.
|
||||||
function unwrapWidget(widgetInstance: WidgetInstance): UnwrappedWidget | undefined;
|
function unwrapWidget(widgetInstance: WidgetInstance): UnwrappedWidget | undefined;
|
||||||
|
|
@ -175,6 +179,7 @@
|
||||||
getProps: (props, index) => ({
|
getProps: (props, index) => ({
|
||||||
...props,
|
...props,
|
||||||
action: () => widgetValueCommitAndUpdate(index, undefined, true),
|
action: () => widgetValueCommitAndUpdate(index, undefined, true),
|
||||||
|
actionDragDrop: () => widgetValueDragDrop(index),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
IconLabel: {
|
IconLabel: {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
import IconLabel from "/src/components/widgets/labels/IconLabel.svelte";
|
import IconLabel from "/src/components/widgets/labels/IconLabel.svelte";
|
||||||
import type { IconName, IconSize } from "/src/icons";
|
import type { IconName, IconSize } from "/src/icons";
|
||||||
import type { ActionShortcut } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { ActionShortcut } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
@ -16,6 +17,9 @@
|
||||||
export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||||
// Callbacks
|
// Callbacks
|
||||||
export let action: (e?: MouseEvent) => void;
|
export let action: (e?: MouseEvent) => void;
|
||||||
|
// Fired when a draggable item is dropped onto this button. Setting this also flags the button as a valid drop target
|
||||||
|
// (the consumer of the drag interaction looks for `[data-drag-droppable]` and dispatches a `dragdrop` event on it).
|
||||||
|
export let actionDragDrop: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
let className = "";
|
let className = "";
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
@ -24,6 +28,14 @@
|
||||||
$: extraClasses = Object.entries(classes)
|
$: extraClasses = Object.entries(classes)
|
||||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
|
// Element-level listener for the `dragdrop` custom event that consumers dispatch when something is dropped on this button
|
||||||
|
let buttonElement: HTMLButtonElement | undefined;
|
||||||
|
function handleDragDrop() {
|
||||||
|
actionDragDrop?.();
|
||||||
|
}
|
||||||
|
onMount(() => buttonElement?.addEventListener("dragdrop", handleDragDrop));
|
||||||
|
onDestroy(() => buttonElement?.removeEventListener("dragdrop", handleDragDrop));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -31,11 +43,14 @@
|
||||||
class:hover-icon={hoverIcon && !disabled}
|
class:hover-icon={hoverIcon && !disabled}
|
||||||
class:disabled
|
class:disabled
|
||||||
class:emphasized
|
class:emphasized
|
||||||
|
class:drag-droppable={Boolean(actionDragDrop)}
|
||||||
|
bind:this={buttonElement}
|
||||||
on:click={action}
|
on:click={action}
|
||||||
{disabled}
|
{disabled}
|
||||||
data-tooltip-label={tooltipLabel}
|
data-tooltip-label={tooltipLabel}
|
||||||
data-tooltip-description={tooltipDescription}
|
data-tooltip-description={tooltipDescription}
|
||||||
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
|
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
|
||||||
|
data-drag-droppable={actionDragDrop ? "" : undefined}
|
||||||
tabindex={emphasized ? -1 : 0}
|
tabindex={emphasized ? -1 : 0}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,13 @@ impl EditorWrapper {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fire a widget's drag-drop action (e.g. when a draggable item is dropped on a button)
|
||||||
|
#[wasm_bindgen(js_name = widgetValueDragDrop)]
|
||||||
|
pub fn widget_value_drag_drop(&self, layout_target: LayoutTarget, widget_id: u64) {
|
||||||
|
let widget_id = WidgetId(widget_id);
|
||||||
|
self.dispatch(LayoutMessage::WidgetValueDragDrop { layout_target, widget_id });
|
||||||
|
}
|
||||||
|
|
||||||
/// Closes out the current transaction (drag-end / text-commit end), so emits during a slider drag collapse into one history step instead of N
|
/// Closes out the current transaction (drag-end / text-commit end), so emits during a slider drag collapse into one history step instead of N
|
||||||
#[wasm_bindgen(js_name = endTransaction)]
|
#[wasm_bindgen(js_name = endTransaction)]
|
||||||
pub fn end_transaction(&self) {
|
pub fn end_transaction(&self) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue