Skip to content
Open
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/portfolio/portfolio_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ pub enum PortfolioMessage {
mouse: Option<(f64, f64)>,
parent_and_insert_index: Option<(LayerNodeIdentifier, usize)>,
},
ImportSvgAsNewDocument {
name: Option<String>,
svg: String,
},
PrevDocument,
SetActivePanel {
panel: PanelType,
Expand Down
15 changes: 15 additions & 0 deletions editor/src/messages/portfolio/portfolio_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,21 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
});
}
}
PortfolioMessage::ImportSvgAsNewDocument { name, svg } => {
// Create a new document explicitly
responses.add(PortfolioMessage::NewDocumentWithName { name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()) });

responses.add(DocumentMessage::PasteSvg {
name,
svg,
mouse: None,
parent_and_insert_index: None,
});

// After graph runs, wrap contents in artboard and zoom to fit
responses.add(DeferMessage::AfterGraphRun { messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()] });
responses.add(DeferMessage::AfterNavigationReady { messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()] });
}
PortfolioMessage::PrevDocument => {
if let Some(active_document_id) = self.active_document_id {
let len = self.document_ids.len();
Expand Down
49 changes: 48 additions & 1 deletion frontend/src/components/panels/Document.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import { updateBoundsOfViewports } from "@graphite/utility-functions/viewports";

import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import Graph from "@graphite/components/views/Graph.svelte";
Expand Down Expand Up @@ -94,6 +96,17 @@
$: canvasWidthScaledRoundedToEven = canvasWidthScaled && (canvasWidthScaled % 2 === 1 ? canvasWidthScaled + 1 : canvasWidthScaled);
$: canvasHeightScaledRoundedToEven = canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled);

// Menu shown when an SVG is dropped on to the canvas
let svgDropMenu: { open: boolean; x: number; y: number; name: string; svg: string } = { open: false, x: 0, y: 0, name: "", svg: "" };

function importSvgAsNewDocument(name: string, svg: string) {
const handleWithImport = editor.handle as unknown as { importSvgAsNewDocument?: (name: string | undefined, svg: string) => void };
handleWithImport.importSvgAsNewDocument?.(name, svg);
}
function importSvgAsLayerAt(x: number, y: number, name: string, svg: string) {
editor.handle.pasteSvg(name, svg, x, y);
}

$: toolShelfTotalToolsAndSeparators = ((layoutGroup) => {
if (!isWidgetSpanRow(layoutGroup)) return undefined;

Expand Down Expand Up @@ -141,7 +154,13 @@

if (file.type.includes("svg")) {
const svgData = await file.text();
editor.handle.pasteSvg(file.name, svgData, x, y);
// Menu shown at drop location
if (typeof x === "number" && typeof y === "number") {
svgDropMenu = { open: true, x, y, name: file.name, svg: svgData };
} else {
//Paste normally
editor.handle.pasteSvg(file.name, svgData, x, y);
}
return;
}

Expand Down Expand Up @@ -597,6 +616,34 @@
</LayoutRow>
</LayoutCol>

{#if svgDropMenu.open}
<FloatingMenu open={true} type="Cursor" class="svg-drop-menu" styles={{ left: svgDropMenu.x + "px", top: svgDropMenu.y + "px" }}>
<MenuList
open={true}
entries={[
[
{
label: "Import as New Layer",
value: "layer",
action: () => {
importSvgAsLayerAt(svgDropMenu.x, svgDropMenu.y, svgDropMenu.name, svgDropMenu.svg);
svgDropMenu.open = false;
},
},
{
label: "Import as New Document",
value: "document",
action: () => {
importSvgAsNewDocument(svgDropMenu.name, svgDropMenu.svg);
svgDropMenu.open = false;
},
},
],
]}
/>
</FloatingMenu>
{/if}

<style lang="scss" global>
.document {
height: 100%;
Expand Down
38 changes: 36 additions & 2 deletions frontend/src/components/window/workspace/Panel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
export let styles: Record<string, string | number | undefined> = {};

let tabElements: (LayoutRow | undefined)[] = [];
let tabGroupElement: LayoutRow | undefined;

function platformModifiers(reservedKey: boolean): LayoutKeysGroup {
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
Expand Down Expand Up @@ -93,15 +94,48 @@
});
}

// To handle dropping SVGs onto the empty tab space so that they open as a new document
function dropOnTabBar(e: DragEvent) {
const { dataTransfer } = e;
if (!dataTransfer) return;

// Determine if the pointer is over the empty area of the tab bar (to the right of the last tab)
const tabGroupDiv = tabGroupElement?.div?.();
if (!tabGroupDiv) return;

const groupBounds = tabGroupDiv.getBoundingClientRect();
const tabDivs = tabElements.map((t) => t?.div?.()).filter((el): el is HTMLDivElement => Boolean(el));
const lastTabRight = tabDivs.length ? tabDivs[tabDivs.length - 1]!.getBoundingClientRect().right : groupBounds.left;
const overEmptySpace = e.clientX > lastTabRight && e.clientX < groupBounds.right;

// If not over the empty part of the tab bar, ignore
if (!overEmptySpace) return;

e.preventDefault();

Array.from(dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile();
if (!file) return;

if (file.type.includes("svg")) {
const svgData = await file.text();
// Call the API `importSvgAsNewDocument`
const handleWithImport = editor.handle as unknown as { importSvgAsNewDocument?: (name: string | undefined, svg: string) => void };
handleWithImport.importSvgAsNewDocument?.(file.name, svgData);
return;
}
});
}

export async function scrollTabIntoView(newIndex: number) {
await tick();
tabElements[newIndex]?.div?.()?.scrollIntoView();
}
</script>

<LayoutCol on:pointerdown={() => panelType && editor.handle.setActivePanel(panelType)} class={`panel ${className}`.trim()} {classes} style={styleName} {styles}>
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }}>
<LayoutRow class="tab-group" scrollableX={true}>
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }} on:dragover={(e) => e.preventDefault()} on:drop={dropOnTabBar}>
<LayoutRow class="tab-group" scrollableX={true} bind:this={tabGroupElement}>
{#each tabLabels as tabLabel, tabIndex}
<LayoutRow
class="tab"
Expand Down
6 changes: 6 additions & 0 deletions frontend/wasm/src/editor_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,12 @@ impl EditorHandle {
self.dispatch(message);
}

/// Import an SVG as a new document directly (used for dropping on tab bar empty area or choosing the menu option)
#[wasm_bindgen(js_name = importSvgAsNewDocument)]
pub fn import_svg_as_new_document(&self, name: Option<String>, svg: String) {
self.dispatch(PortfolioMessage::ImportSvgAsNewDocument { name, svg });
}

/// Toggle visibility of a layer or node given its node ID
#[wasm_bindgen(js_name = toggleNodeVisibilityLayerPanel)]
pub fn toggle_node_visibility_layer(&self, id: u64) {
Expand Down