Desktop: Add the transparent viewport hole punch and hook up window button plumbing (#2949)

This commit is contained in:
Keavon Chambers 2025-07-28 02:13:32 -07:00 committed by GitHub
parent d9de1a1c73
commit 66cd7a3b76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 300 additions and 77 deletions

View File

@ -14,6 +14,7 @@ pub struct Dispatcher {
#[derive(Debug, Default)]
pub struct DispatcherMessageHandlers {
animation_message_handler: AnimationMessageHandler,
app_window_message_handler: AppWindowMessageHandler,
broadcast_message_handler: BroadcastMessageHandler,
debug_message_handler: DebugMessageHandler,
dialog_message_handler: DialogMessageHandler,
@ -129,6 +130,9 @@ impl Dispatcher {
Message::Animation(message) => {
self.message_handlers.animation_message_handler.process_message(message, &mut queue, ());
}
Message::AppWindow(message) => {
self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ());
}
Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()),
Message::Debug(message) => {
self.message_handlers.debug_message_handler.process_message(message, &mut queue, ());

View File

@ -0,0 +1,9 @@
use crate::messages::prelude::*;
#[impl_message(Message, AppWindow)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum AppWindowMessage {
AppWindowMinimize,
AppWindowMaximize,
AppWindowClose,
}

View File

@ -0,0 +1,52 @@
use crate::messages::app_window::AppWindowMessage;
use crate::messages::prelude::*;
use graphite_proc_macros::{ExtractField, message_handler_data};
#[derive(Debug, Clone, Default, ExtractField)]
pub struct AppWindowMessageHandler {
platform: AppWindowPlatform,
maximized: bool,
viewport_hole_punch_active: bool,
}
#[message_handler_data]
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
match message {
AppWindowMessage::AppWindowMinimize => {
self.platform = if self.platform == AppWindowPlatform::Mac {
AppWindowPlatform::Windows
} else {
AppWindowPlatform::Mac
};
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
}
AppWindowMessage::AppWindowMaximize => {
self.maximized = !self.maximized;
responses.add(FrontendMessage::UpdateMaximized { maximized: self.maximized });
self.viewport_hole_punch_active = !self.viewport_hole_punch_active;
responses.add(FrontendMessage::UpdateViewportHolePunch {
active: self.viewport_hole_punch_active,
});
}
AppWindowMessage::AppWindowClose => {
self.platform = AppWindowPlatform::Web;
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
}
}
}
fn actions(&self) -> ActionList {
actions!(AppWindowMessageDiscriminant;)
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum AppWindowPlatform {
#[default]
Web,
Windows,
Mac,
Linux,
}

View File

@ -0,0 +1,7 @@
mod app_window_message;
pub mod app_window_message_handler;
#[doc(inline)]
pub use app_window_message::{AppWindowMessage, AppWindowMessageDiscriminant};
#[doc(inline)]
pub use app_window_message_handler::AppWindowMessageHandler;

View File

@ -1,4 +1,5 @@
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::{
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, Transform,
@ -309,4 +310,13 @@ pub enum FrontendMessage {
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdatePlatform {
platform: AppWindowPlatform,
},
UpdateMaximized {
maximized: bool,
},
UpdateViewportHolePunch {
active: bool,
},
}

View File

@ -9,6 +9,8 @@ pub enum Message {
#[child]
Animation(AnimationMessage),
#[child]
AppWindow(AppWindowMessage),
#[child]
Broadcast(BroadcastMessage),
#[child]
Debug(DebugMessage),

View File

@ -1,6 +1,7 @@
//! The root-level messages forming the first layer of the message system architecture.
pub mod animation;
pub mod app_window;
pub mod broadcast;
pub mod debug;
pub mod dialog;

View File

@ -107,15 +107,15 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let compatible_type = first_layer.and_then(|layer| {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
graph_layer.horizontal_layer_flow().nth(1).and_then(|node_id| {
graph_layer.horizontal_layer_flow().nth(1).map(|node_id| {
let (output_type, _) = document.network_interface.output_type(&node_id, 0, &[]);
Some(format!("type:{}", output_type.nested_type()))
format!("type:{}", output_type.nested_type())
})
});
let is_compatible = compatible_type.as_deref() == Some("type:Instances<VectorData>");
let is_modifiable = first_layer.map_or(false, |layer| {
let is_modifiable = first_layer.is_some_and(|layer| {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
matches!(graph_layer.find_input("Path", 1), Some(TaggedValue::VectorModification(_)))
});

View File

@ -53,7 +53,7 @@ impl From<String> for PanelType {
"Layers" => PanelType::Layers,
"Properties" => PanelType::Properties,
"Spreadsheet" => PanelType::Spreadsheet,
_ => panic!("Unknown panel type: {}", value),
_ => panic!("Unknown panel type: {value}"),
}
}
}

View File

@ -3,6 +3,7 @@ pub use crate::utility_traits::{ActionList, AsMessage, HierarchicalTree, Message
pub use crate::utility_types::{DebugMessageTree, MessageData};
// Message, MessageData, MessageDiscriminant, MessageHandler
pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscriminant, AnimationMessageHandler};
pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler};
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler};
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageContext, ExportDialogMessageDiscriminant, ExportDialogMessageHandler};

View File

@ -21,11 +21,17 @@
<noscript>JavaScript is required</noscript>
<style>
body {
background: #222;
height: 100%;
margin: 0;
}
body::before {
content: "";
position: absolute;
inset: 0;
background: #222;
}
body::after {
content: "";
display: block;

View File

@ -9,6 +9,7 @@
import { createLocalizationManager } from "@graphite/io-managers/localization";
import { createPanicManager } from "@graphite/io-managers/panic";
import { createPersistenceManager } from "@graphite/io-managers/persistence";
import { createAppWindowState } from "@graphite/state-providers/app-window";
import { createDialogState } from "@graphite/state-providers/dialog";
import { createDocumentState } from "@graphite/state-providers/document";
import { createFontsState } from "@graphite/state-providers/fonts";
@ -36,6 +37,8 @@
setContext("nodeGraph", nodeGraph);
let portfolio = createPortfolioState(editor);
setContext("portfolio", portfolio);
let appWindow = createAppWindowState(editor);
setContext("appWindow", appWindow);
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
createClipboardManager(editor);
@ -58,10 +61,11 @@
});
</script>
<MainWindow />
<MainWindow platform={$appWindow.platform} maximized={$appWindow.maximized} viewportHolePunch={$appWindow.viewportHolePunch} />
<style lang="scss" global>
// Disable the spinning loading indicator
body::before,
body::after {
content: none !important;
}
@ -206,10 +210,16 @@
height: 100%;
background: var(--color-2-mildblack);
overscroll-behavior: none;
-webkit-user-select: none; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
-webkit-user-select: none; // Still required by Safari as of 2025
user-select: none;
}
// Needed for the viewport hole punch on desktop
html:has(body > .viewport-hole-punch),
body:has(> .viewport-hole-punch) {
background: none;
}
// The default value of `auto` from the CSS spec is a footgun with flexbox layouts:
// https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size
* {

View File

@ -16,6 +16,7 @@
UpdateMouseCursor,
isWidgetSpanRow,
} from "@graphite/messages";
import type { AppWindowState } from "@graphite/state-providers/app-window";
import type { DocumentState } from "@graphite/state-providers/document";
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
@ -34,6 +35,7 @@
let viewport: HTMLDivElement | undefined;
const editor = getContext<Editor>("editor");
const appWindow = getContext<AppWindowState>("appWindow");
const document = getContext<DocumentState>("document");
// Interactive text editing
@ -514,13 +516,13 @@
<RulerInput origin={rulerOrigin.x} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Horizontal" bind:this={rulerHorizontal} />
</LayoutRow>
{/if}
<LayoutRow class="viewport-container-inner">
<LayoutRow class="viewport-container-inner-1">
{#if rulersVisible}
<LayoutCol class="ruler-or-scrollbar">
<RulerInput origin={rulerOrigin.y} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Vertical" bind:this={rulerVertical} />
</LayoutCol>
{/if}
<LayoutCol class="viewport-container-inner" styles={{ cursor: canvasCursor }}>
<LayoutCol class="viewport-container-inner-2" styles={{ cursor: canvasCursor }} data-viewport-container>
{#if cursorEyedropper}
<EyedropperPreview
colorChoice={cursorEyedropperPreviewColorChoice}
@ -531,6 +533,7 @@
y={cursorTop}
/>
{/if}
{#if !$appWindow.viewportHolePunch}
<div class="viewport" on:pointerdown={(e) => canvasPointerDown(e)} bind:this={viewport} data-viewport>
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html artworkSvg}
@ -550,6 +553,7 @@
>
</canvas>
</div>
{/if}
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork={`${$document.fadeArtwork}%`} data-graph>
<Graph />
</div>
@ -593,7 +597,8 @@
.control-bar {
height: 32px;
flex: 0 0 auto;
margin: 0 4px;
padding: 0 4px; // Padding (instead of margin) is needed for the viewport hole punch on desktop
background: var(--color-3-darkgray); // Needed for the viewport hole punch on desktop
.spacer {
min-width: 40px;
@ -632,6 +637,7 @@
.tool-shelf {
flex: 0 0 auto;
justify-content: space-between;
background: var(--color-3-darkgray); // Needed for the viewport hole punch on desktop
.tools {
flex: 0 1 auto;
@ -713,6 +719,7 @@
.ruler-or-scrollbar {
flex: 0 0 auto;
background: var(--color-3-darkgray); // Needed for the viewport hole punch on desktop
}
.ruler-corner {
@ -743,7 +750,8 @@
margin-right: 16px;
}
.viewport-container-inner {
.viewport-container-inner-1,
.viewport-container-inner-2 {
flex: 1 1 100%;
position: relative;

View File

@ -1,18 +1,17 @@
<script lang="ts" context="module">
export type ApplicationPlatform = "Windows" | "Mac" | "Linux" | "Web";
</script>
<script lang="ts">
import type { AppWindowPlatform } from "@graphite/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import StatusBar from "@graphite/components/window/status-bar/StatusBar.svelte";
import TitleBar from "@graphite/components/window/title-bar/TitleBar.svelte";
import Workspace from "@graphite/components/window/workspace/Workspace.svelte";
let platform: ApplicationPlatform = "Web";
let maximized: true;
export let platform: AppWindowPlatform;
export let maximized: boolean;
export let viewportHolePunch: boolean;
</script>
<LayoutCol class="main-window">
<LayoutCol class="main-window" classes={{ "viewport-hole-punch": viewportHolePunch }}>
<TitleBar {platform} {maximized} />
<Workspace />

View File

@ -1,12 +1,8 @@
<script lang="ts" context="module">
export type Platform = "Windows" | "Mac" | "Linux" | "Web";
</script>
<script lang="ts">
import { getContext, onMount } from "svelte";
import type { Editor } from "@graphite/editor";
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@graphite/messages";
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, type AppWindowPlatform, UpdateMenuBarLayout } from "@graphite/messages";
import type { PortfolioState } from "@graphite/state-providers/portfolio";
import { platformIsMac } from "@graphite/utility-functions/platform";
@ -17,7 +13,7 @@
import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte";
import WindowTitle from "@graphite/components/window/title-bar/WindowTitle.svelte";
export let platform: Platform;
export let platform: AppWindowPlatform;
export let maximized: boolean;
const editor = getContext<Editor>("editor");
@ -73,7 +69,7 @@
<!-- Menu bar (or on Mac: window buttons) -->
<LayoutRow class="left">
{#if platform === "Mac"}
<WindowButtonsMac {maximized} />
<WindowButtonsMac />
{:else}
{#each entries as entry}
<TextButton label={entry.label} icon={entry.icon} menuListChildren={entry.children} action={entry.action} flush={true} />

View File

@ -1,13 +1,17 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Editor } from "@graphite/editor";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
export let maximized = false;
const editor = getContext<Editor>("editor");
</script>
<LayoutRow class="window-buttons-mac">
<div class="close" title="Close" />
<div class="minimize" title={maximized ? "Minimize" : "Maximize"} />
<div class="zoom" title="Zoom" />
<div class="close" on:click={() => editor.handle.appWindowClose()} />
<div class="minimize" on:click={() => editor.handle.appWindowMinimize()} />
<div class="zoom" on:click={() => editor.handle.appWindowMaximize()} />
</LayoutRow>
<style lang="scss" global>

View File

@ -1,23 +1,32 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Editor } from "@graphite/editor";
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const editor = getContext<Editor>("editor");
const fullscreen = getContext<FullscreenState>("fullscreen");
$: requestFullscreenHotkeys = fullscreen.keyboardLockApiSupported && !$fullscreen.keyboardLocked;
async function handleClick() {
async function handleClick(e: MouseEvent) {
// TODO: Remove this debugging option to switch from web to desktop window buttons
if (e.ctrlKey && e.shiftKey && e.altKey) {
editor.handle.appWindowMinimize();
editor.handle.appWindowMinimize();
return;
}
if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen();
else fullscreen.enterFullscreen();
}
</script>
<LayoutRow class="window-buttons-web" on:click={() => handleClick()} tooltip={($fullscreen.windowFullscreen ? "Exit" : "Enter") + " Fullscreen (F11)"}>
<LayoutRow class="window-buttons-web" on:click={handleClick} tooltip={$fullscreen.windowFullscreen ? "Exit Fullscreen (F11)" : "Enter Fullscreen (F11)"}>
{#if requestFullscreenHotkeys}
<TextLabel italic={true}>Go fullscreen to access all hotkeys</TextLabel>
{/if}

View File

@ -1,23 +1,23 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Editor } from "@graphite/editor";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
export let maximized = false;
export let maximized;
const editor = getContext<Editor>("editor");
</script>
<LayoutRow class="window-button windows minimize" tooltip="Minimize">
<LayoutRow class="window-button windows" tooltip="Minimize" on:click={() => editor.handle.appWindowMinimize()}>
<IconLabel icon={"WindowButtonWinMinimize"} />
</LayoutRow>
{#if !maximized}
<LayoutRow class="window-button windows maximize" tooltip="Maximize">
<IconLabel icon={"WindowButtonWinMaximize"} />
<LayoutRow class="window-button windows" tooltip={maximized ? "Restore Down" : "Maximize"} on:click={() => editor.handle.appWindowMaximize()}>
<IconLabel icon={maximized ? "WindowButtonWinRestoreDown" : "WindowButtonWinMaximize"} />
</LayoutRow>
{:else}
<LayoutRow class="window-button windows restore-down" tooltip="Restore Down">
<IconLabel icon={"WindowButtonWinRestoreDown"} />
</LayoutRow>
{/if}
<LayoutRow class="window-button windows close" tooltip="Close">
<LayoutRow class="window-button windows" tooltip="Close" on:click={() => editor.handle.appWindowClose()}>
<IconLabel icon={"WindowButtonWinClose"} />
</LayoutRow>
@ -39,7 +39,7 @@
}
}
&.close:hover {
&:last-of-type:hover {
background: #e81123;
}
}

View File

@ -42,6 +42,13 @@
export let clickAction: ((index: number) => void) | undefined = undefined;
export let closeAction: ((index: number) => void) | undefined = undefined;
let className = "";
export { className as class };
export let classes: Record<string, boolean> = {};
let styleName = "";
export { styleName as style };
export let styles: Record<string, string | number | undefined> = {};
let tabElements: (LayoutRow | undefined)[] = [];
function platformModifiers(reservedKey: boolean): LayoutKeysGroup {
@ -90,7 +97,7 @@
}
</script>
<LayoutCol class="panel" on:pointerdown={() => panelType && editor.handle.setActivePanel(panelType)}>
<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}>
{#each tabLabels as tabLabel, tabIndex}
@ -194,6 +201,7 @@
.tab-bar {
height: 28px;
min-height: auto;
background: var(--color-1-nearblack); // Needed for the viewport hole punch on desktop
&.min-widths .tab-group .tab {
min-width: 120px;
@ -336,5 +344,11 @@
}
}
}
// Needed for the viewport hole punch on desktop
.viewport-hole-punch &.document-panel,
.viewport-hole-punch &.document-panel .panel-body:not(:has(.empty-panel)) {
background: none;
}
}
</style>

View File

@ -137,6 +137,7 @@
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["content"] }} data-subdivision-name="content">
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["document"] }} data-subdivision-name="document">
<Panel
class="document-panel"
panelType={$portfolio.documents.length > 0 ? "Document" : undefined}
tabCloseButtons={true}
tabMinWidths={true}
@ -176,8 +177,9 @@
flex: 1 1 100%;
.workspace-grid-subdivision {
min-height: 28px;
position: relative;
flex: 1 1 0;
min-height: 28px;
&.folded {
flex-grow: 0;
@ -196,5 +198,15 @@
cursor: ew-resize;
}
}
// Needed for the viewport hole punch on desktop
.viewport-hole-punch & .workspace-grid-subdivision:has(.panel.document-panel)::after {
content: "";
position: absolute;
inset: 6px;
border-radius: 6px;
box-shadow: 0 0 0 calc(100vw + 100vh) var(--color-2-mildblack);
z-index: -1;
}
}
</style>

View File

@ -169,7 +169,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
potentiallyRestoreCanvasFocus(e);
const { target } = e;
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]");
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-viewport-container], [data-node-graph]");
const inDialog = target instanceof Element && target.closest("[data-dialog] [data-floating-menu-content]");
const inContextMenu = target instanceof Element && target.closest("[data-context-menu]");
const inTextInput = target === textToolInteractiveInputElement;
@ -219,7 +219,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
// Allow only events within the viewport or node graph boundaries
const { target } = e;
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]");
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-viewport-container], [data-node-graph]");
if (!(isTargetingCanvas instanceof Element)) return;
// Allow only repeated increments of double-clicks (not 1, 3, 5, etc.)
@ -256,7 +256,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
function onWheelScroll(e: WheelEvent) {
const { target } = e;
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]");
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-viewport-container], [data-node-graph]");
// Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element
// There seems to be no possible way to properly employ the browser's smooth scrolling interpolation
@ -502,7 +502,9 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
function potentiallyRestoreCanvasFocus(e: Event) {
const { target } = e;
const newInCanvasArea = (target instanceof Element && target.closest("[data-viewport], [data-graph]")) instanceof Element && !targetIsTextField(window.document.activeElement || undefined);
const newInCanvasArea =
(target instanceof Element && target.closest("[data-viewport], [data-viewport-container], [data-graph]")) instanceof Element &&
!targetIsTextField(window.document.activeElement || undefined);
if (!canvasFocused && newInCanvasArea) {
canvasFocused = true;
app?.focus();

View File

@ -349,6 +349,21 @@ export class TriggerIndexedDbRemoveDocument extends JsMessage {
documentId!: string;
}
export type AppWindowPlatform = "Web" | "Windows" | "Mac" | "Linux";
export class UpdatePlatform extends JsMessage {
@Transform(({ value }: { value: AppWindowPlatform }) => value)
readonly platform!: AppWindowPlatform;
}
export class UpdateMaximized extends JsMessage {
readonly maximized!: boolean;
}
export class UpdateViewportHolePunch extends JsMessage {
readonly active!: boolean;
}
export class UpdateInputHints extends JsMessage {
@Type(() => HintInfo)
readonly hintData!: HintData;
@ -1670,29 +1685,32 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateEyedropperSamplingState,
UpdateGraphFadeArtwork,
UpdateGraphViewOverlay,
UpdateSpreadsheetState,
UpdateImportReorderIndex,
UpdateImportsExports,
UpdateInputHints,
UpdateInSelectedNetwork,
UpdateLayersPanelBottomBarLayout,
UpdateLayersPanelControlBarLeftLayout,
UpdateLayersPanelControlBarRightLayout,
UpdateLayersPanelBottomBarLayout,
UpdateLayerWidths,
UpdateMaximized,
UpdateMenuBarLayout,
UpdateMouseCursor,
UpdateNodeGraphNodes,
UpdateVisibleNodes,
UpdateNodeGraphWires,
UpdateNodeGraphTransform,
UpdateNodeGraphControlBarLayout,
UpdateNodeGraphNodes,
UpdateNodeGraphSelection,
UpdateNodeGraphTransform,
UpdateNodeGraphWires,
UpdateNodeThumbnail,
UpdateOpenDocumentsList,
UpdatePlatform,
UpdatePropertyPanelSectionsLayout,
UpdateSpreadsheetLayout,
UpdateSpreadsheetState,
UpdateToolOptionsLayout,
UpdateToolShelfLayout,
UpdateViewportHolePunch,
UpdateVisibleNodes,
UpdateWirePathInProgress,
UpdateWorkingColorsLayout,
} as const;

View File

@ -0,0 +1,40 @@
/* eslint-disable max-classes-per-file */
import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor";
import { type AppWindowPlatform, UpdatePlatform, UpdateMaximized, UpdateViewportHolePunch } from "@graphite/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createAppWindowState(editor: Editor) {
const { subscribe, update } = writable({
platform: "Web" as AppWindowPlatform,
maximized: false,
viewportHolePunch: false,
});
// Set up message subscriptions on creation
editor.subscriptions.subscribeJsMessage(UpdatePlatform, (updatePlatform) => {
update((state) => {
state.platform = updatePlatform.platform;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateMaximized, (maximized) => {
update((state) => {
state.maximized = maximized.maximized;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateViewportHolePunch, (viewportHolePunch) => {
update((state) => {
state.viewportHolePunch = viewportHolePunch.active;
return state;
});
});
return {
subscribe,
};
}
export type AppWindowState = ReturnType<typeof createAppWindowState>;

View File

@ -103,7 +103,6 @@ export function createPortfolioState(editor: Editor) {
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
}
});
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => {
update((state) => {
state.spreadsheetOpen = updateSpreadsheetState.open;
@ -111,7 +110,6 @@ export function createPortfolioState(editor: Editor) {
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetLayout, (updateSpreadsheetLayout) => {
update((state) => {
patchWidgetLayout(state.spreadsheetWidgets, updateSpreadsheetLayout);

View File

@ -270,6 +270,27 @@ impl EditorHandle {
}
}
/// Minimizes the application window to the taskbar or dock
#[wasm_bindgen(js_name = appWindowMinimize)]
pub fn app_window_minimize(&self) {
let message = AppWindowMessage::AppWindowMinimize;
self.dispatch(message);
}
/// Toggles minimizing or restoring down the application window
#[wasm_bindgen(js_name = appWindowMaximize)]
pub fn app_window_maximize(&self) {
let message = AppWindowMessage::AppWindowMaximize;
self.dispatch(message);
}
/// Closes the application window
#[wasm_bindgen(js_name = appWindowClose)]
pub fn app_window_close(&self) {
let message = AppWindowMessage::AppWindowClose;
self.dispatch(message);
}
/// Displays a dialog with an error message
#[wasm_bindgen(js_name = errorDialog)]
pub fn error_dialog(&self, title: String, description: String) {

View File

@ -43,7 +43,7 @@ impl Context {
trace: wgpu::Trace::Off,
})
.await
.unwrap();
.ok()?;
let info = adapter.get_info();
// skip this on LavaPipe temporarily