diff --git a/console/src-tauri/src/main.rs b/console/src-tauri/src/main.rs index 88c4fa2ceb..26b8cedabf 100644 --- a/console/src-tauri/src/main.rs +++ b/console/src-tauri/src/main.rs @@ -4,9 +4,6 @@ #[cfg(target_os = "macos")] extern crate cocoa; -#[cfg(target_os = "macos")] -extern crate objc; - use device_query::{DeviceEvents, DeviceQuery, DeviceState, MouseState}; use std::thread; use std::time::Duration; @@ -55,7 +52,6 @@ fn set_transparent_titlebar(_: &Window, _: bool) {} fn main() { tauri::Builder::default() - .plugin(tauri_plugin_store::Builder::new().build()) .on_page_load(|window, _| { set_transparent_titlebar(&window.window(), true); return; diff --git a/console/src/Console.tsx b/console/src/Console.tsx new file mode 100644 index 0000000000..ed42ad262a --- /dev/null +++ b/console/src/Console.tsx @@ -0,0 +1,151 @@ +// Copyright 2024 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +import "@/index.css"; +import "@synnaxlabs/media/dist/style.css"; +import "@synnaxlabs/pluto/dist/style.css"; + +import { Provider } from "@synnaxlabs/drift/react"; +import { type Haul, Pluto, type state, type Triggers } from "@synnaxlabs/pluto"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { type ReactElement, useCallback, useEffect } from "react"; +import { useDispatch } from "react-redux"; + +import { Channel } from "@/channel"; +import { Cluster } from "@/cluster"; +import { Confirm } from "@/confirm"; +import { Docs } from "@/docs"; +import { ErrorOverlayWithoutStore, ErrorOverlayWithStore } from "@/error/Overlay"; +import { LabJack } from "@/hardware/labjack"; +import { NI } from "@/hardware/ni"; +import { OPC } from "@/hardware/opc"; +import { Label } from "@/label"; +import { Layout } from "@/layout"; +import { Layouts } from "@/layouts"; +import { LinePlot } from "@/lineplot"; +import { Log } from "@/log"; +import { Ontology } from "@/ontology"; +import { Permissions } from "@/permissions"; +import { Range } from "@/range"; +import { Schematic } from "@/schematic"; +import { SERVICES } from "@/services"; +import { store } from "@/store"; +import { User } from "@/user"; +import { Version } from "@/version"; +import { Vis } from "@/vis"; +import WorkerURL from "@/worker?worker&url"; +import { Workspace } from "@/workspace"; + +const LAYOUT_RENDERERS: Record = { + ...Layouts.LAYOUTS, + ...Docs.LAYOUTS, + ...Workspace.LAYOUTS, + ...Schematic.LAYOUTS, + ...LinePlot.LAYOUTS, + ...LabJack.LAYOUTS, + ...OPC.LAYOUTS, + ...Range.LAYOUTS, + ...Cluster.LAYOUTS, + ...NI.LAYOUTS, + ...Channel.LAYOUTS, + ...Version.LAYOUTS, + ...Confirm.LAYOUTS, + ...Label.LAYOUTS, + ...User.LAYOUTS, + ...Permissions.LAYOUTS, + ...Log.LAYOUTS, +}; + +const CONTEXT_MENU_RENDERERS: Record = { + ...Schematic.CONTEXT_MENUS, + ...LinePlot.CONTEXT_MENUS, +}; + +const PREVENT_DEFAULT_TRIGGERS: Triggers.Trigger[] = [ + ["Control", "P"], + ["Control", "Shift", "P"], + ["Control", "MouseLeft"], + ["Control", "W"], +]; + +const TRIGGERS_PROVIDER_PROPS: Triggers.ProviderProps = { + preventDefaultOn: PREVENT_DEFAULT_TRIGGERS, + preventDefaultOptions: { double: true }, +}; + +const client = new QueryClient(); + +const useHaulState: state.PureUse = () => { + const hauled = Layout.useSelectHauling(); + const dispatch = useDispatch(); + const onHauledChange = useCallback( + (state: Haul.DraggingState) => dispatch(Layout.setHauled(state)), + [dispatch], + ); + return [hauled, onHauledChange]; +}; + +const useBlockDefaultDropBehavior = (): void => + useEffect(() => { + const doc = document.documentElement; + doc.addEventListener("dragover", (e) => e.preventDefault()); + doc.addEventListener("drop", (e) => e.preventDefault()); + return () => { + doc.removeEventListener("dragover", (e) => e.preventDefault()); + doc.removeEventListener("drop", (e) => e.preventDefault()); + }; + }, []); + +const MainUnderContext = (): ReactElement => { + const theme = Layout.useThemeProvider(); + const cluster = Cluster.useSelect(); + const activeRange = Range.useSelect(); + useBlockDefaultDropBehavior(); + return ( + + + + + + + + ); +}; + +export const Console = (): ReactElement => ( + + + + + + + + + + + + + +); diff --git a/console/src/isDev.ts b/console/src/isDev.ts new file mode 100644 index 0000000000..bb523d6c4e --- /dev/null +++ b/console/src/isDev.ts @@ -0,0 +1,10 @@ +// Copyright 2024 Synnax Labs, Inc. +// +// Use of this software is governed by the Business Source License included in the file +// licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with the Business Source +// License, use of this software will be governed by the Apache License, Version 2.0, +// included in the file licenses/APL.txt. + +export const isDev = () => (window as any).isDev; diff --git a/console/src/main.tsx b/console/src/main.tsx index edbf5b7f4f..5f060ad7f3 100644 --- a/console/src/main.tsx +++ b/console/src/main.tsx @@ -7,150 +7,10 @@ // License, use of this software will be governed by the Apache License, Version 2.0, // included in the file licenses/APL.txt. -import "@/index.css"; -import "@synnaxlabs/media/dist/style.css"; -import "@synnaxlabs/pluto/dist/style.css"; +import { createRoot } from "react-dom/client"; -import { Provider } from "@synnaxlabs/drift/react"; -import { type Haul, Pluto, type state, type Triggers } from "@synnaxlabs/pluto"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { type ReactElement, useCallback, useEffect } from "react"; -import ReactDOM from "react-dom/client"; -import { useDispatch } from "react-redux"; - -import { Channel } from "@/channel"; -import { Cluster } from "@/cluster"; -import { Confirm } from "@/confirm"; -import { Docs } from "@/docs"; -import { ErrorOverlayWithoutStore, ErrorOverlayWithStore } from "@/error/Overlay"; -import { LabJack } from "@/hardware/labjack"; -import { NI } from "@/hardware/ni"; -import { OPC } from "@/hardware/opc"; -import { Label } from "@/label"; -import { Layout } from "@/layout"; -import { Layouts } from "@/layouts"; -import { LinePlot } from "@/lineplot"; -import { Log } from "@/log"; -import { Ontology } from "@/ontology"; -import { Permissions } from "@/permissions"; -import { Range } from "@/range"; -import { Schematic } from "@/schematic"; -import { SERVICES } from "@/services"; -import { store } from "@/store"; -import { User } from "@/user"; -import { Version } from "@/version"; -import { Vis } from "@/vis"; -import WorkerURL from "@/worker?worker&url"; -import { Workspace } from "@/workspace"; - -const LAYOUT_RENDERERS: Record = { - ...Layouts.LAYOUTS, - ...Docs.LAYOUTS, - ...Workspace.LAYOUTS, - ...Schematic.LAYOUTS, - ...LinePlot.LAYOUTS, - ...LabJack.LAYOUTS, - ...OPC.LAYOUTS, - ...Range.LAYOUTS, - ...Cluster.LAYOUTS, - ...NI.LAYOUTS, - ...Channel.LAYOUTS, - ...Version.LAYOUTS, - ...Confirm.LAYOUTS, - ...Label.LAYOUTS, - ...User.LAYOUTS, - ...Permissions.LAYOUTS, - ...Log.LAYOUTS, -}; - -const CONTEXT_MENU_RENDERERS: Record = { - ...Schematic.CONTEXT_MENUS, - ...LinePlot.CONTEXT_MENUS, -}; - -const PREVENT_DEFAULT_TRIGGERS: Triggers.Trigger[] = [ - ["Control", "P"], - ["Control", "Shift", "P"], - ["Control", "MouseLeft"], - ["Control", "W"], -]; - -const TRIGGERS_PROVIDER_PROPS: Triggers.ProviderProps = { - preventDefaultOn: PREVENT_DEFAULT_TRIGGERS, - preventDefaultOptions: { double: true }, -}; - -const client = new QueryClient(); - -const useHaulState: state.PureUse = () => { - const hauled = Layout.useSelectHauling(); - const dispatch = useDispatch(); - const onHauledChange = useCallback( - (state: Haul.DraggingState) => dispatch(Layout.setHauled(state)), - [dispatch], - ); - return [hauled, onHauledChange]; -}; - -const useBlockDefaultDropBehavior = (): void => - useEffect(() => { - const doc = document.documentElement; - doc.addEventListener("dragover", (e) => e.preventDefault()); - doc.addEventListener("drop", (e) => e.preventDefault()); - return () => { - doc.removeEventListener("dragover", (e) => e.preventDefault()); - doc.removeEventListener("drop", (e) => e.preventDefault()); - }; - }, []); - -const MainUnderContext = (): ReactElement => { - const theme = Layout.useThemeProvider(); - const cluster = Cluster.useSelect(); - const activeRange = Range.useSelect(); - useBlockDefaultDropBehavior(); - return ( - - - - - - - - ); -}; - -const Main = (): ReactElement => ( - - - - - - - - - - - - - -); +import { Console } from "@/Console"; const rootEl = document.getElementById("root") as HTMLElement; -ReactDOM.createRoot(rootEl).render(
); +createRoot(rootEl).render(); diff --git a/console/src/persist/state.ts b/console/src/persist/state.ts index 189705a370..2ad7a87ef9 100644 --- a/console/src/persist/state.ts +++ b/console/src/persist/state.ts @@ -15,7 +15,7 @@ import { type UnknownAction, } from "@reduxjs/toolkit"; import { MAIN_WINDOW } from "@synnaxlabs/drift"; -import { debounce, deep, type UnknownRecord } from "@synnaxlabs/x"; +import { debounce, deep, TimeSpan, type UnknownRecord } from "@synnaxlabs/x"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { createTauriKV } from "@/persist/kv"; @@ -57,6 +57,8 @@ export const hardClearAndReload = () => { .finally(window.location.reload); }; +const PERSIST_DEBOUNCE = TimeSpan.milliseconds(250).milliseconds; + export const open = async ({ exclude = [], initial, @@ -102,7 +104,7 @@ export const open = async ({ await db.set(DB_VERSION_KEY, { version }).catch(console.error); await db.delete(persistedStateKey(version - KEEP_HISTORY)).catch(console.error); })(); - }, 500); + }, PERSIST_DEBOUNCE); let state = (await db.get(persistedStateKey(version))) ?? undefined; if (state != null && migrator != null) { diff --git a/console/src/store.ts b/console/src/store.ts index 8933fda427..d2985aca63 100644 --- a/console/src/store.ts +++ b/console/src/store.ts @@ -14,6 +14,7 @@ import { type deep } from "@synnaxlabs/x"; import { Cluster } from "@/cluster"; import { Docs } from "@/docs"; +import { isDev } from "@/isDev"; import { Layout } from "@/layout"; import { LinePlot } from "@/lineplot"; import { Log } from "@/log"; @@ -87,7 +88,7 @@ export type RootAction = export type RootStore = Store; -const DEFAULT_WINDOW_PROPS: Omit = { visible: false }; +const DEFAULT_WINDOW_PROPS: Omit = { visible: isDev() }; export const migrateState = (prev: RootState): RootState => { console.log("--------------- Migrating State ---------------"); @@ -125,7 +126,8 @@ const newStore = async (): Promise => { if (preloadedState != null && Drift.SLICE_NAME in preloadedState) { const windows = preloadedState[Drift.SLICE_NAME].windows; Object.keys(windows).forEach((key) => { - windows[key].visible = false; + if (windows[key].key === "prerender") return; + windows[key].visible = isDev(); windows[key].focusCount = 0; windows[key].centerCount = 0; }); diff --git a/console/vite.config.ts b/console/vite.config.ts index 1ac6785e65..bc327ba458 100644 --- a/console/vite.config.ts +++ b/console/vite.config.ts @@ -44,4 +44,7 @@ export default defineConfig({ // is loaded directly from disc instead of OTN chunkSizeWarningLimit: 10000 /* kbs */, }, + define: { + isDev, + }, }); diff --git a/drift/package.json b/drift/package.json index 5490da6220..2fc7f66ab6 100644 --- a/drift/package.json +++ b/drift/package.json @@ -25,6 +25,7 @@ "@reduxjs/toolkit": "^2.3.0", "@synnaxlabs/x": "workspace:*", "@tauri-apps/api": "^2.1.0", + "async-mutex": "^0.5.0", "proxy-memoize": "2.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/drift/src/middleware.ts b/drift/src/middleware.ts index f2707316b1..8961e74cf0 100644 --- a/drift/src/middleware.ts +++ b/drift/src/middleware.ts @@ -16,6 +16,7 @@ import { type Tuple, type UnknownAction, } from "@reduxjs/toolkit"; +import { Mutex } from "async-mutex"; import { log } from "@/debug"; import { type Runtime } from "@/runtime"; @@ -25,6 +26,7 @@ import { isDriftAction, type LabelPayload, type MaybeKeyPayload, + reloadWindow, setWindowError, setWindowProps, shouldEmit, @@ -37,6 +39,8 @@ import { validateAction } from "@/validate"; export type Middlewares = ReadonlyArray>; +const mu = new Mutex(); + /** * Redux middleware that conditionally does two things: * @@ -74,7 +78,10 @@ export const middleware = const isDrift = isDriftAction(action.type); // If the runtime is updating its own props, no need to sync. - const shouldSync = isDrift && action.type !== setWindowProps.type; + const shouldSync = + isDrift && + action.type !== setWindowProps.type && + action.type !== reloadWindow.type; let prevS: SliceState | null = null; if (isDrift) { @@ -93,7 +100,7 @@ export const middleware = // Wrap everything in an async closure eto ensure that we synchronize before // before emitting to other windows. - void (async (): Promise => { + mu.runExclusive(async (): Promise => { try { if (prevS !== null && nextS !== null) await sync(prevS, nextS, runtime, debug); if (shouldEmit_) await runtime.emit({ action }); @@ -107,7 +114,7 @@ export const middleware = }); dispatch(setWindowError({ key: label, message: (err as Error).message })); } - })(); + }); return res; }; @@ -119,15 +126,17 @@ export const middleware = * @param runtime - The runtime of the current window. * @returns a middleware function to be passed to `configureStore`. */ -export const configureMiddleware = < - S extends StoreState, - A extends CoreAction = UnknownAction, - M extends Middlewares = Middlewares, ->( - mw: M | ((def: GetDefaultMiddleware) => M) | undefined, - runtime: Runtime, - debug: boolean = false, -): ((def: GetDefaultMiddleware) => M) => (def) => { +export const configureMiddleware = + < + S extends StoreState, + A extends CoreAction = UnknownAction, + M extends Middlewares = Middlewares, + >( + mw: M | ((def: GetDefaultMiddleware) => M) | undefined, + runtime: Runtime, + debug: boolean = false, + ): ((def: GetDefaultMiddleware) => M) => + (def) => { const base = mw != null ? (typeof mw === "function" ? mw(def) : mw) : def(); return [middleware(runtime, debug), ...base] as unknown as M; }; diff --git a/drift/src/state.ts b/drift/src/state.ts index 98bb66d2d9..37dcf027a4 100644 --- a/drift/src/state.ts +++ b/drift/src/state.ts @@ -225,8 +225,8 @@ const slice = createSlice({ if (s.label !== MAIN_WINDOW && !s.config.enablePrerender) return; const prerenderLabel = id.id(); s.windows[prerenderLabel] = { - ...INITIAL_PRERENDER_WINDOW_STATE, ...s.config.defaultWindowProps, + ...INITIAL_PRERENDER_WINDOW_STATE, }; }, createWindow: (s: SliceState, { payload }: PayloadAction) => { @@ -302,7 +302,7 @@ const slice = createSlice({ const win = s.windows[a.payload.label]; if (win == null || win.processCount > 0) return; win.stage = "reloading"; - window.location.reload(); + setTimeout(() => window.location.reload(), 50); }), registerProcess: assertLabel(incrementCounter("processCount")), completeProcess: assertLabel((s, a) => { diff --git a/drift/src/tauri/index.ts b/drift/src/tauri/index.ts index 4aca8525c0..5951a38d06 100644 --- a/drift/src/tauri/index.ts +++ b/drift/src/tauri/index.ts @@ -8,7 +8,7 @@ // included in the file licenses/APL.txt. import { type Action, type UnknownAction } from "@reduxjs/toolkit"; -import { debounce as debounceF, type dimensions, type xy } from "@synnaxlabs/x"; +import { debounce as debounceF, dimensions, xy } from "@synnaxlabs/x"; import { emit, type Event as TauriEvent, @@ -278,8 +278,8 @@ export class TauriRuntime [ maximized: await window.isMaximized(), visible, minimized: !visible, - position: await parsePosition(await window.innerPosition(), scaleFactor), - size: await parseSize(await window.innerSize(), scaleFactor), + position: parsePosition(await window.innerPosition(), scaleFactor), + size: parseSize(await window.innerSize(), scaleFactor), }; return setWindowProps(nextProps); }, @@ -318,7 +318,7 @@ const newWindowPropsHandlers = (): HandlerEntry[] => [ handler: async (window) => { const scaleFactor = await window?.scaleFactor(); if (scaleFactor == null) return null; - const position = await parsePosition(await window.innerPosition(), scaleFactor); + const position = parsePosition(await window.innerPosition(), scaleFactor); const visible = await window.isVisible(); const nextProps: SetWindowPropsPayload = { label: window.label, @@ -346,18 +346,8 @@ const newWindowPropsHandlers = (): HandlerEntry[] => [ }, ]; -const parsePosition = async ( - position: PhysicalPosition, - scaleFactor: number, -): Promise => { - const logical = position.toLogical(scaleFactor); - return { x: logical.x, y: logical.y }; -}; +const parsePosition = (position: PhysicalPosition, scaleFactor: number): xy.XY => + xy.scale(position, 1 / scaleFactor); -const parseSize = async ( - size: PhysicalSize, - scaleFactor: number, -): Promise => { - const logical = size.toLogical(scaleFactor); - return { width: logical.width, height: logical.height }; -}; +const parseSize = (size: PhysicalSize, scaleFactor: number): dimensions.Dimensions => + dimensions.scale(size, 1 / scaleFactor); diff --git a/pluto/src/vis/canvas/Canvas.tsx b/pluto/src/vis/canvas/Canvas.tsx index 9a600b63e6..232818daa6 100644 --- a/pluto/src/vis/canvas/Canvas.tsx +++ b/pluto/src/vis/canvas/Canvas.tsx @@ -97,8 +97,17 @@ export const Canvas = Aether.wrap( // We want to trigger a re-render when the window is focused or blurred to ensure // that we wake up sleeping render contexts. useEffect(() => { - window.addEventListener("focus", () => setState((p) => ({ ...p }))); - window.addEventListener("blur", () => setState((p) => ({ ...p }))); + const handler = () => { + if (!canvases.current.bootstrapped) return; + setState((p) => ({ + ...p, + glCanvas: undefined, + upper2dCanvas: undefined, + lower2dCanvas: undefined, + })); + }; + window.addEventListener("focus", handler); + window.addEventListener("blur", handler); }, [setState]); const refCallback = useCallback( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0b7a3a692..d147748e40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,7 +219,7 @@ importers: version: 5.1.0 '@reduxjs/toolkit': specifier: ^2.3.0 - version: 2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + version: 2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) '@synnaxlabs/client': specifier: workspace:* version: link:../client/ts @@ -422,13 +422,16 @@ importers: dependencies: '@reduxjs/toolkit': specifier: ^2.3.0 - version: 2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + version: 2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) '@synnaxlabs/x': specifier: workspace:* version: link:../x/ts '@tauri-apps/api': specifier: ^2.1.0 version: 2.1.0 + async-mutex: + specifier: ^0.5.0 + version: 0.5.0 proxy-memoize: specifier: 2.0.3 version: 2.0.3 @@ -477,7 +480,7 @@ importers: dependencies: '@reduxjs/toolkit': specifier: ^2.3.0 - version: 2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + version: 2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) '@synnaxlabs/drift': specifier: workspace:* version: link:../.. @@ -8253,7 +8256,7 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@reduxjs/toolkit@2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + '@reduxjs/toolkit@2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)': dependencies: immer: 10.1.1 redux: 5.0.1