Skip to content
Merged
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
4 changes: 4 additions & 0 deletions editor/src/messages/layout/layout_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ pub enum LayoutMessage {
widget_id: WidgetId,
value: serde_json::Value,
},
WidgetValueDragDrop {
layout_target: LayoutTarget,
widget_id: WidgetId,
},
}
13 changes: 13 additions & 0 deletions editor/src/messages/layout/layout_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Comment thread
Keavon marked this conversation as resolved.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
Expand Down
48 changes: 45 additions & 3 deletions frontend/src/components/panels/Layers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -394,6 +415,7 @@
draggingData = undefined;
fakeHighlightOfNotYetSelectedLayerBeingDragged = undefined;
dragInPanel = false;
dragDropTarget = undefined;
}

function draggingMouseDown(e: MouseEvent) {
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/widgets/WidgetSpan.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -175,6 +179,7 @@
getProps: (props, index) => ({
...props,
action: () => widgetValueCommitAndUpdate(index, undefined, true),
actionDragDrop: () => widgetValueDragDrop(index),
}),
},
IconLabel: {
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/components/widgets/buttons/IconButton.svelte
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 };
Expand All @@ -24,18 +28,29 @@
$: 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));
Comment thread
Keavon marked this conversation as resolved.
</script>

<button
class={`icon-button size-${size} ${className} ${extraClasses}`.trim()}
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}
>
Expand Down
7 changes: 7 additions & 0 deletions frontend/wrapper/src/editor_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading