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,
|
||||
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 } => {
|
||||
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)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_commit: WidgetCallback<()>,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_drag_drop: WidgetCallback<IconButton>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
|
|
|
|||
|
|
@ -3288,17 +3288,23 @@ impl DocumentMessageHandler {
|
|||
let group_folder_type = GroupFolderType::Layer;
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
||||
})
|
||||
.on_drag_drop(|_| {
|
||||
let group_folder_type = GroupFolderType::Layer;
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
||||
})
|
||||
.disabled(!has_selection)
|
||||
.widget_instance(),
|
||||
IconButton::new("NewLayer", 24)
|
||||
.tooltip_label("New Layer")
|
||||
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::CreateEmptyFolder))
|
||||
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
|
||||
.on_drag_drop(|_| DocumentMessage::DuplicateSelectedLayers.into())
|
||||
.widget_instance(),
|
||||
IconButton::new("Trash", 24)
|
||||
.tooltip_label("Delete Selected")
|
||||
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::DeleteSelectedLayers))
|
||||
.on_update(|_| DocumentMessage::DeleteSelectedLayers.into())
|
||||
.on_drag_drop(|_| DocumentMessage::DeleteSelectedLayers.into())
|
||||
.disabled(!has_selection)
|
||||
.widget_instance(),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@
|
|||
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
|
||||
let justFinishedDrag = false; // Used to prevent click events after a drag
|
||||
let dragInPanel = false;
|
||||
let dragDropTarget: HTMLElement | undefined = undefined;
|
||||
|
||||
// Interactive clipping
|
||||
let layerToClipUponClick: LayerListingInfo | undefined = undefined;
|
||||
|
|
@ -258,7 +259,7 @@
|
|||
|
||||
let markerHeight = 0;
|
||||
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;
|
||||
Array.from(treeChildren).forEach((treeChild) => {
|
||||
const indexAttribute = treeChild.getAttribute("data-index");
|
||||
|
|
@ -358,6 +359,18 @@
|
|||
|
||||
// Perform drag calculations if a drag is occurring
|
||||
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 = () => {
|
||||
if (internalDragState && !$nodeGraph.selected.includes(internalDragState.layerId)) {
|
||||
selectLayer(internalDragState.listing, false, false);
|
||||
|
|
@ -369,7 +382,15 @@
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
// Commit the move
|
||||
|
|
@ -394,6 +415,7 @@
|
|||
draggingData = undefined;
|
||||
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
|
||||
dragInPanel = false;
|
||||
dragDropTarget = undefined;
|
||||
}
|
||||
|
||||
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>
|
||||
{/if}
|
||||
</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" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
|
|
@ -698,6 +720,26 @@
|
|||
&:not(:has(*)) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@
|
|||
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.
|
||||
// The overload declares the precise correlated return type while the implementation uses broader types.
|
||||
function unwrapWidget(widgetInstance: WidgetInstance): UnwrappedWidget | undefined;
|
||||
|
|
@ -175,6 +179,7 @@
|
|||
getProps: (props, index) => ({
|
||||
...props,
|
||||
action: () => widgetValueCommitAndUpdate(index, undefined, true),
|
||||
actionDragDrop: () => widgetValueDragDrop(index),
|
||||
}),
|
||||
},
|
||||
IconLabel: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import IconLabel from "/src/components/widgets/labels/IconLabel.svelte";
|
||||
import type { IconName, IconSize } from "/src/icons";
|
||||
import type { ActionShortcut } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
|
|
@ -16,6 +17,9 @@
|
|||
export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||
// Callbacks
|
||||
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 = "";
|
||||
export { className as class };
|
||||
|
|
@ -24,6 +28,14 @@
|
|||
$: extraClasses = Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.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>
|
||||
|
||||
<button
|
||||
|
|
@ -31,11 +43,14 @@
|
|||
class:hover-icon={hoverIcon && !disabled}
|
||||
class:disabled
|
||||
class:emphasized
|
||||
class:drag-droppable={Boolean(actionDragDrop)}
|
||||
bind:this={buttonElement}
|
||||
on:click={action}
|
||||
{disabled}
|
||||
data-tooltip-label={tooltipLabel}
|
||||
data-tooltip-description={tooltipDescription}
|
||||
data-tooltip-shortcut={tooltipShortcut?.shortcut ? JSON.stringify(tooltipShortcut.shortcut) : undefined}
|
||||
data-drag-droppable={actionDragDrop ? "" : undefined}
|
||||
tabindex={emphasized ? -1 : 0}
|
||||
{...$$restProps}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -343,6 +343,13 @@ impl EditorWrapper {
|
|||
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
|
||||
#[wasm_bindgen(js_name = endTransaction)]
|
||||
pub fn end_transaction(&self) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue