tldraw icon indicating copy to clipboard operation
tldraw copied to clipboard

[Feature]: Improve layout composition flexibility between UI and Editor

Open ruiconti opened this issue 2 years ago • 3 comments

What's the feature?

Context: I am writing a custom UI and it inevitably needs to useEditor to interact with Editor state. However, it seems that the EditorContext is not exposed in any way, so we need to structure the toolbar as being a child of TldrawEditor —the component in which EditorContext.Provider is declared.

What I'm trying to achieve is decouple the Editor's Canvas and the Toolbar, and rely on the browser's layout engine to place them as I see fit (e.g using flexbox, relative positioning, etc), instead of trying to stub things with absolutes.

One way to work around this is to try to layout things within TldrawEditor:

globalCss`
     .tl-container {
         position: relative;
         display: flex;
     }
`

const CustomEditor = () => {
    <TldrawEditor> {/* .tl-container */}
        <CustomUI>
        <Canvas>
    </TldrawEditor>
}

However, it seems that tools rely on the TldrawEditor's ref-element (the one tagged with .tl-container) positioning to draw shapes, so if I try to move the canvas around within the TldrawEditor, shapes are displayed with this unexpected offset.

CleanShot_2023-10-17_at_11 55 57

Proposed solution

However, it'd be best if consumers were not bound by this implicit layout expectation from tools, as this greatly limits how one can layout the canvas and a custom UI. Ideally, tldraw should export EditorContext so that consumers can layout however they see fit.

For instance, this is what I'm doing to get the job done:

diff --git a/node_modules/@tldraw/editor/dist-cjs/index.d.ts b/node_modules/@tldraw/editor/dist-cjs/index.d.ts
index 6c05c8f..110b6de 100644
--- a/node_modules/@tldraw/editor/dist-cjs/index.d.ts
+++ b/node_modules/@tldraw/editor/dist-cjs/index.d.ts
@@ -5571,6 +5571,9 @@ export declare function useContainer(): HTMLDivElement;
 /** @public */
 export declare const useEditor: () => Editor;
 
+/** @public */
+export declare const EditorContext: React.Context<Editor | null>;
+
 /** @public */
 export declare function useIsCropping(shapeId: TLShapeId): boolean;
 
diff --git a/node_modules/@tldraw/editor/dist-cjs/index.js b/node_modules/@tldraw/editor/dist-cjs/index.js
index aa1e3bb..f369f9a 100644
--- a/node_modules/@tldraw/editor/dist-cjs/index.js
+++ b/node_modules/@tldraw/editor/dist-cjs/index.js
@@ -54,6 +54,7 @@ __export(src_exports, {
   EVENT_NAME_MAP: () => import_event_types.EVENT_NAME_MAP,
   Edge2d: () => import_Edge2d.Edge2d,
   Editor: () => import_Editor.Editor,
+  EditorContext: () => import_useEditor.EditorContext,
   Ellipse2d: () => import_Ellipse2d.Ellipse2d,
   ErrorBoundary: () => import_ErrorBoundary.ErrorBoundary,
   ErrorScreen: () => import_TldrawEditor.ErrorScreen,
diff --git a/node_modules/@tldraw/editor/dist-esm/index.d.mts b/node_modules/@tldraw/editor/dist-esm/index.d.mts
index 6c05c8f..110b6de 100644
--- a/node_modules/@tldraw/editor/dist-esm/index.d.mts
+++ b/node_modules/@tldraw/editor/dist-esm/index.d.mts
@@ -5571,6 +5571,9 @@ export declare function useContainer(): HTMLDivElement;
 /** @public */
 export declare const useEditor: () => Editor;
 
+/** @public */
+export declare const EditorContext: React.Context<Editor | null>;
+
 /** @public */
 export declare function useIsCropping(shapeId: TLShapeId): boolean;
 
diff --git a/node_modules/@tldraw/editor/dist-esm/index.mjs b/node_modules/@tldraw/editor/dist-esm/index.mjs
index 68c95c9..57ac5bb 100644
--- a/node_modules/@tldraw/editor/dist-esm/index.mjs
+++ b/node_modules/@tldraw/editor/dist-esm/index.mjs
@@ -136,7 +136,7 @@ import {
 } from "./lib/editor/types/event-types.mjs";
 import { useContainer } from "./lib/hooks/useContainer.mjs";
 import { getCursor } from "./lib/hooks/useCursor.mjs";
-import { useEditor } from "./lib/hooks/useEditor.mjs";
+import { useEditor, EditorContext } from "./lib/hooks/useEditor.mjs";
 import { useIsCropping } from "./lib/hooks/useIsCropping.mjs";
 import { useIsDarkMode } from "./lib/hooks/useIsDarkMode.mjs";
 import { useIsEditing } from "./lib/hooks/useIsEditing.mjs";
@@ -295,6 +295,7 @@ export {
   EVENT_NAME_MAP,
   Edge2d,
   Editor,
+  EditorContext,
   Ellipse2d,
   ErrorBoundary,
   ErrorScreen,

Contact Details

[email protected]

Code of Conduct

  • [X] I agree to follow this project's Code of Conduct

ruiconti avatar Oct 20 '23 14:10 ruiconti

I think this is easy enough and would bring amazing customization

irg1008 avatar Nov 07 '23 08:11 irg1008

Hey! You can definitely move the UI out of the TldrawEditor component. To make that work just use the onMount prop, which should give you the editor once it's mounted. Created a quick example (based on the CustomUiExample from the repo): https://github.com/tldraw/tldraw/blob/16f9054f71b421c8904bf724caa78ba7b3e7c06f/apps/examples/src/examples/CustomUiExample/CustomUiExample.tsx#L11

Would this work for your use case?

MitjaBezensek avatar Nov 08 '23 11:11 MitjaBezensek

Hey! You can definitely move the UI out of the TldrawEditor component. To make that work just use the onMount prop, which should give you the editor once it's mounted. Created a quick example (based on the CustomUiExample from the repo):

https://github.com/tldraw/tldraw/blob/16f9054f71b421c8904bf724caa78ba7b3e7c06f/apps/examples/src/examples/CustomUiExample/CustomUiExample.tsx#L11

Would this work for your use case?

That is a good point. We currently do this dance of picking up the Editor instance onMount, and broadcast it over as an EditorContext.Provider value, so it can be referenced outside of TldrawEditor. We do this because we still want to leverage other key functionality in a decoupled toolbar that relies on EditorContext:

useActions, for undo, redo and others: https://github.com/tldraw/tldraw/blob/431ce73476f6116f3234b4d667fd3752f140ff89/packages/tldraw/src/lib/ui/hooks/useActions.tsx#L73-L74

useKeyboardShortcuts and useNativeClipboardEvents, both of which need to be on the same sub-tree as the toolbar, because it depends on ActionContextProvider and UiEventsProvider:

https://github.com/tldraw/tldraw/blob/431ce73476f6116f3234b4d667fd3752f140ff89/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts#L18-L20

https://github.com/tldraw/tldraw/blob/431ce73476f6116f3234b4d667fd3752f140ff89/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts#L624-L626

Personally and maintenance-wise, I'd love to not have to do this dance of capturing the Editor instance on a onMount callback. It adds unnecessary complexity as this is me trying to work around Tldraw and React so I can layout components the way I need. However, this could be solved by exposing the Editor state dependency via parameters rather than React contexts.

ruiconti avatar Nov 16 '23 13:11 ruiconti

Folding this up into a newer issue https://github.com/tldraw/tldraw/issues/3860

mimecuvalo avatar Jul 23 '24 14:07 mimecuvalo