diff --git a/apps/studio/src/routes/editor/LayersPanel/LayersTab.tsx b/apps/studio/src/routes/editor/LayersPanel/LayersTab.tsx index 518e6b99bf..132ec20270 100644 --- a/apps/studio/src/routes/editor/LayersPanel/LayersTab.tsx +++ b/apps/studio/src/routes/editor/LayersPanel/LayersTab.tsx @@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { type NodeApi, Tree, type TreeApi } from 'react-arborist'; import useResizeObserver from 'use-resize-observer'; -import RightClickMenu from '../RightClickMenu'; +import { RightClickMenu } from '../RightClickMenu'; import TreeNode from './Tree/TreeNode'; import TreeRow from './Tree/TreeRow'; diff --git a/apps/studio/src/routes/editor/RightClickMenu/index.tsx b/apps/studio/src/routes/editor/RightClickMenu/index.tsx index a095ab5d4e..b810b608c6 100644 --- a/apps/studio/src/routes/editor/RightClickMenu/index.tsx +++ b/apps/studio/src/routes/editor/RightClickMenu/index.tsx @@ -227,5 +227,3 @@ export const RightClickMenu = observer(({ children }: RightClickMenuProps) => { ); }); - -export default RightClickMenu; diff --git a/apps/studio/src/routes/editor/WebviewArea/GestureScreen.tsx b/apps/studio/src/routes/editor/WebviewArea/GestureScreen.tsx index ab808b5b75..2cc94c29d7 100644 --- a/apps/studio/src/routes/editor/WebviewArea/GestureScreen.tsx +++ b/apps/studio/src/routes/editor/WebviewArea/GestureScreen.tsx @@ -2,12 +2,12 @@ import { useEditorEngine } from '@/components/Context'; import { getRelativeMousePositionToWebview } from '@/lib/editor/engine/overlay/utils'; import { EditorMode } from '@/lib/models'; import { MouseAction } from '@onlook/models/editor'; -import type { DomElement, DropElementProperties, ElementPosition } from '@onlook/models/element'; +import type { DomElement, ElementPosition } from '@onlook/models/element'; import { cn } from '@onlook/ui/utils'; import throttle from 'lodash/throttle'; import { observer } from 'mobx-react-lite'; import { useCallback, useEffect, useMemo } from 'react'; -import RightClickMenu from '../RightClickMenu'; +import { RightClickMenu } from '../RightClickMenu'; interface GestureScreenProps { webviewRef: React.RefObject; diff --git a/apps/web/src/app/project/[id]/_components/canvas/frame/gesture.tsx b/apps/web/src/app/project/[id]/_components/canvas/frame/gesture.tsx new file mode 100644 index 0000000000..e9b72ab89e --- /dev/null +++ b/apps/web/src/app/project/[id]/_components/canvas/frame/gesture.tsx @@ -0,0 +1,202 @@ +import { useEditorEngine } from '@/components/store'; +import { getRelativeMousePositionToWebview } from '@/components/store/editor/engine/overlay/utils'; +import { EditorMode, type MouseAction } from '@onlook/models/editor'; +import type { ElementPosition } from '@onlook/models/element'; +import { cn } from '@onlook/ui/utils'; +import throttle from 'lodash/throttle'; +import { observer } from 'mobx-react-lite'; +import { useCallback, useEffect, useMemo } from 'react'; +import { RightClickMenu } from './right-click'; + +export const GestureScreen = observer(() => { + const editorEngine = useEditorEngine(); + const isResizing = false; + + const getWebview = useCallback((): Electron.WebviewTag => { + // const webview = webviewRef.current as Electron.WebviewTag | null; + // if (!webview) { + // throw Error('No webview found'); + // } + // return webview; + }, []); + + const getRelativeMousePosition = useCallback( + (e: React.MouseEvent): ElementPosition => { + const webview = getWebview(); + return getRelativeMousePositionToWebview(e, webview); + }, + [getWebview], + ); + + const handleMouseEvent = useCallback( + async (e: React.MouseEvent, action: MouseAction) => { + // const webview = getWebview(); + // const pos = getRelativeMousePosition(e); + // const el: DomElement = await webview.executeJavaScript( + // `window.api?.getElementAtLoc(${pos.x}, ${pos.y}, ${action === MouseAction.MOUSE_DOWN || action === MouseAction.DOUBLE_CLICK})`, + // ); + // if (!el) { + // return; + // } + + // switch (action) { + // case MouseAction.MOVE: + // editorEngine.elements.mouseover(el, webview); + // if (e.altKey) { + // editorEngine.elements.showMeasurement(); + // } else { + // editorEngine.overlay.removeMeasurement(); + // } + // break; + // case MouseAction.MOUSE_DOWN: + // if (el.tagName.toLocaleLowerCase() === 'body') { + // editorEngine.webview.select(webview); + // return; + // } + // // Ignore right-clicks + // if (e.button == 2) { + // break; + // } + // if (editorEngine.text.isEditing) { + // editorEngine.text.end(); + // } + // if (e.shiftKey) { + // editorEngine.elements.shiftClick(el, webview); + // } else { + // editorEngine.move.start(el, pos, webview); + // editorEngine.elements.click([el], webview); + // } + // break; + // case MouseAction.DOUBLE_CLICK: + // editorEngine.text.start(el, webview); + // break; + // } + }, + [getWebview, getRelativeMousePosition, editorEngine], + ); + + const throttledMouseMove = useMemo( + () => + throttle((e: React.MouseEvent) => { + // if (editorEngine.state.move.isDragging) { + // editorEngine.state.move.drag(e, getRelativeMousePosition); + // } else if ( + // editorEngine.state.editorMode === EditorMode.DESIGN || + // ((editorEngine.state.editorMode === EditorMode.INSERT_DIV || + // editorEngine.state.editorMode === EditorMode.INSERT_TEXT || + // editorEngine.state.editorMode === EditorMode.INSERT_IMAGE) && + // !editorEngine.insert.isDrawing) + // ) { + // handleMouseEvent(e, MouseAction.MOVE); + // } else if (editorEngine.insert.isDrawing) { + // editorEngine.insert.draw(e); + // } + }, 16), + [editorEngine, getRelativeMousePosition, handleMouseEvent], + ); + + useEffect(() => { + return () => { + throttledMouseMove.cancel(); + }; + }, [throttledMouseMove]); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + // const webview = getWebview(); + // editorEngine.webview.deselectAll(); + // editorEngine.webview.select(webview); + }, + [getWebview, editorEngine.webview], + ); + + function handleDoubleClick(e: React.MouseEvent) { + // if (editorEngine.state.editorMode !== EditorMode.DESIGN) { + // return; + // } + // handleMouseEvent(e, MouseAction.DOUBLE_CLICK); + } + + function handleMouseDown(e: React.MouseEvent) { + // if (editorEngine.state.editorMode === EditorMode.DESIGN) { + // handleMouseEvent(e, MouseAction.MOUSE_DOWN); + // } else if ( + // editorEngine.state.editorMode === EditorMode.INSERT_DIV || + // editorEngine.state.editorMode === EditorMode.INSERT_TEXT || + // editorEngine.state.editorMode === EditorMode.INSERT_IMAGE + // ) { + // editorEngine.insert.start(e); + // } + } + + async function handleMouseUp(e: React.MouseEvent) { + // editorEngine.insert.end(e, webviewRef.current); + // editorEngine.move.end(e); + } + + const handleDragOver = (e: React.DragEvent) => { + // e.preventDefault(); + // e.stopPropagation(); + // handleMouseEvent(e, MouseAction.MOVE); + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // try { + // const propertiesData = e.dataTransfer.getData('application/json'); + // if (!propertiesData) { + // console.error('No element properties in drag data'); + // return; + // } + + // const properties = JSON.parse(propertiesData); + + // if (properties.type === 'image') { + // const webview = getWebview(); + // const dropPosition = getRelativeMousePosition(e); + // await editorEngine.insert.insertDroppedImage(webview, dropPosition, properties); + // } else { + // const webview = getWebview(); + // const dropPosition = getRelativeMousePosition(e); + // await editorEngine.insert.insertDroppedElement(webview, dropPosition, properties); + // } + + // editorEngine.state.editorMode = EditorMode.DESIGN; + // } catch (error) { + // console.error('drop operation failed:', error); + // } + }; + + const gestureScreenClassName = useMemo(() => { + return cn( + 'absolute inset-0 bg-transparent', + editorEngine.state.editorMode === EditorMode.PREVIEW && !isResizing ? 'hidden' : 'visible', + editorEngine.state.editorMode === EditorMode.INSERT_DIV && 'cursor-crosshair', + editorEngine.state.editorMode === EditorMode.INSERT_TEXT && 'cursor-text', + ); + }, [editorEngine.state.editorMode, isResizing]); + + const handleMouseOut = () => { + editorEngine.elements.clearHoveredElement(); + editorEngine.overlay.state.updateHoverRect(null); + } + + return ( + +
+
+ ); +}); diff --git a/apps/web/src/app/project/[id]/_components/canvas/frame/index.tsx b/apps/web/src/app/project/[id]/_components/canvas/frame/index.tsx new file mode 100644 index 0000000000..2c6f28bef0 --- /dev/null +++ b/apps/web/src/app/project/[id]/_components/canvas/frame/index.tsx @@ -0,0 +1,30 @@ +import { FrameType, type Frame, type WebFrame } from "@onlook/models"; +import { observer } from "mobx-react-lite"; +import { GestureScreen } from './gesture'; +import { ResizeHandles } from './resize-handles'; +import { TopBar } from "./top-bar"; +import { WebFrameComponent } from "./web-frame"; + +export const FrameView = observer( + ({ + frame, + }: { + frame: Frame; + }) => { + return ( +
+ + +
+ + {frame.type === FrameType.WEB && } + + {/* {domFailed && shouldShowDomFailed && renderNotRunning()} */} +
+ +
+ ); + }); \ No newline at end of file diff --git a/apps/web/src/app/project/[id]/_components/canvas/frames/resize-handles.tsx b/apps/web/src/app/project/[id]/_components/canvas/frame/resize-handles.tsx similarity index 61% rename from apps/web/src/app/project/[id]/_components/canvas/frames/resize-handles.tsx rename to apps/web/src/app/project/[id]/_components/canvas/frame/resize-handles.tsx index 3864320197..e693b6acd5 100644 --- a/apps/web/src/app/project/[id]/_components/canvas/frames/resize-handles.tsx +++ b/apps/web/src/app/project/[id]/_components/canvas/frame/resize-handles.tsx @@ -1,12 +1,9 @@ import { useEditorEngine } from '@/components/store'; -import type { SizePreset } from '@/components/store/editor/engine/canvas'; +import type { FrameImpl } from '@/components/store/editor/engine/canvas/frame'; import { DefaultSettings } from '@onlook/models/constants'; -import type { FrameSettings } from '@onlook/models/projects'; -import { ToastAction } from '@onlook/ui/toast'; -import { useToast } from '@onlook/ui/use-toast'; import { cn } from '@onlook/ui/utils'; import { observer } from 'mobx-react-lite'; -import { type MouseEvent, useRef, useState } from 'react'; +import { type MouseEvent } from 'react'; enum HandleType { Right = 'right', @@ -15,39 +12,28 @@ enum HandleType { export const ResizeHandles = observer( ({ - settings, + frame }: { - settings: FrameSettings; + frame: FrameImpl; }) => { const editorEngine = useEditorEngine(); - const resizeHandleRef = useRef(null); - const { toast } = useToast(); - - // TODO: Move these to the store - const [size, setSize] = useState(settings.dimension); - const [isResizing, setIsResizing] = useState(false); - const [aspectRatioLocked, setAspectRatioLocked] = useState( - settings.aspectRatioLocked || DefaultSettings.ASPECT_RATIO_LOCKED, - ); - const [selectedPreset, setSelectedPreset] = useState(null); - const [lockedPreset, setLockedPreset] = useState(null); + const aspectRatioLocked = false; + const lockedPreset = false; const startResize = ( - e: MouseEvent, + e: MouseEvent, types: HandleType[], ) => { e.preventDefault(); e.stopPropagation(); - setIsResizing(true); - const startX = e.clientX; const startY = e.clientY; - const startWidth = size.width; - const startHeight = size.height; + const startWidth = frame.dimension.width; + const startHeight = frame.dimension.height; const aspectRatio = startWidth / startHeight; - const resize: any = (e: MouseEvent) => { + const resize = (e: MouseEvent) => { const scale = editorEngine.canvas.scale; let heightDelta = types.includes(HandleType.Bottom) ? (e.clientY - startY) / scale @@ -95,85 +81,55 @@ export const ResizeHandles = observer( } } - setSize({ + frame.dimension = { width: Math.floor(currentWidth), height: Math.floor(currentHeight), - }); - - setSelectedPreset(null); + }; }; - const stopResize = (e: any) => { + const stopResize = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - setIsResizing(false); - - window.removeEventListener('mousemove', resize); - window.removeEventListener('mouseup', stopResize); - }; - - window.addEventListener('mousemove', resize); - window.addEventListener('mouseup', stopResize); - }; - - const handleLockedResize = () => { - const unlockPresetToast = () => { - setLockedPreset(null); + window.removeEventListener('mousemove', resize as unknown as EventListener); + window.removeEventListener('mouseup', stopResize as unknown as EventListener); }; - toast({ - title: 'Preset dimensions locked.', - description: 'Unlock to resize.', - action: ( - - Unlock - - ), - }); + window.addEventListener('mousemove', resize as unknown as EventListener); + window.addEventListener('mouseup', stopResize as unknown as EventListener); }; return (
- lockedPreset ? handleLockedResize() : startResize(e, [HandleType.Bottom]) - } + onMouseDown={(e) => startResize(e, [HandleType.Bottom])} >
- lockedPreset ? handleLockedResize() : startResize(e, [HandleType.Right]) - } + onMouseDown={(e) => startResize(e, [HandleType.Right])} >
- lockedPreset - ? handleLockedResize() - : startResize(e, [HandleType.Right, HandleType.Bottom]) - } + onMouseDown={(e) => startResize(e, [HandleType.Right, HandleType.Bottom])} >
diff --git a/apps/web/src/app/project/[id]/_components/canvas/frame/right-click.tsx b/apps/web/src/app/project/[id]/_components/canvas/frame/right-click.tsx new file mode 100644 index 0000000000..682db1d8cd --- /dev/null +++ b/apps/web/src/app/project/[id]/_components/canvas/frame/right-click.tsx @@ -0,0 +1,237 @@ +import { Hotkey } from '@/components/hotkey'; +import { IDE } from '@/components/ide'; +import { useEditorEngine, useUserManager } from '@/components/store'; +import { DEFAULT_IDE, EditorTabValue } from '@onlook/models'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from '@onlook/ui/context-menu'; +import { Icons } from '@onlook/ui/icons'; +import { Kbd } from '@onlook/ui/kbd'; +import { cn } from '@onlook/ui/utils'; +import { observer } from 'mobx-react-lite'; +import { useEffect, useState } from 'react'; + +interface RightClickMenuProps { + children: React.ReactNode; +} + +interface MenuItem { + label: string; + action: () => void; + hotkey?: Hotkey; + children?: MenuItem[]; + icon: React.ReactNode; + disabled?: boolean; + destructive?: boolean; +} + +export const RightClickMenu = observer(({ children }: RightClickMenuProps) => { + const editorEngine = useEditorEngine(); + const userManager = useUserManager(); + const [menuItems, setMenuItems] = useState([]); + const ide = IDE.fromType(userManager.settings.settings?.editor?.ideType ?? DEFAULT_IDE); + + useEffect(() => { + updateMenuItems(); + }, [ + // editorEngine.elements.selected, + // editorEngine.ast.mappings.layers, + // editorEngine.webviews.selected, + ]); + + const OPEN_DEV_TOOL_ITEM: MenuItem = { + label: 'Open devtool', + action: () => editorEngine.inspect(), + icon: , + hotkey: Hotkey.OPEN_DEV_TOOL, + }; + + const TOOL_ITEMS: MenuItem[] = [ + OPEN_DEV_TOOL_ITEM, + { + label: 'Add to AI Chat', + action: () => { + editorEngine.state.editorPanelTab = EditorTabValue.CHAT; + editorEngine.chat.focusChatInput(); + }, + icon: , + hotkey: Hotkey.ADD_AI_CHAT, + disabled: !editorEngine.elements.selected.length, + }, + { + label: 'New AI Chat', + action: () => { + editorEngine.state.editorPanelTab = EditorTabValue.CHAT; + editorEngine.chat.conversation.startNewConversation(); + editorEngine.chat.focusChatInput(); + }, + icon: , + hotkey: Hotkey.NEW_AI_CHAT, + }, + ]; + + const GROUP_ITEMS: MenuItem[] = [ + { + label: 'Group', + icon: , + // action: () => editorEngine.group.groupSelectedElements(), + // disabled: !editorEngine.group.canGroupElements(), + action: () => { }, + hotkey: Hotkey.GROUP, + }, + { + label: 'Ungroup', + // action: () => editorEngine.group.ungroupSelectedElement(), + // disabled: !editorEngine.group.canUngroupElement(), + action: () => { }, + icon: , + hotkey: Hotkey.UNGROUP, + }, + ]; + + const EDITING_ITEMS: MenuItem[] = [ + { + label: 'Edit text', + // action: () => editorEngine.text.editSelectedElement(), + action: () => { }, + icon: , + hotkey: Hotkey.ENTER, + }, + { + label: 'Copy', + // action: () => editorEngine.copy.copy(), + action: () => { }, + icon: , + hotkey: Hotkey.COPY, + }, + { + label: 'Paste', + // action: () => editorEngine.copy.paste(), + action: () => { }, + icon: , + hotkey: Hotkey.PASTE, + }, + { + label: 'Cut', + // action: () => editorEngine.copy.cut(), + action: () => { }, + icon: , + hotkey: Hotkey.CUT, + }, + { + label: 'Duplicate', + // action: () => editorEngine.copy.duplicate(), + action: () => { }, + icon: , + hotkey: Hotkey.DUPLICATE, + }, + { + label: 'Delete', + // action: () => editorEngine.elements.delete(), + action: () => { }, + icon: , + hotkey: Hotkey.DELETE, + destructive: true, + }, + ]; + + const WINDOW_ITEMS: MenuItem[] = [ + { + label: 'Duplicate', + // action: () => editorEngine.duplicateWindow(), + action: () => { }, + icon: , + hotkey: Hotkey.DUPLICATE, + }, + { + label: 'Delete', + // action: () => editorEngine.deleteWindow(editorEngine.webviews.selected[0].id), + action: () => { }, + icon: , + hotkey: Hotkey.DELETE, + destructive: true, + // disabled: !editorEngine.canDeleteWindow(), + }, + ]; + + const updateMenuItems = () => { + // let instance: string | null = null; + // let root: string | null = null; + + // if (editorEngine.elements.selected.length > 0) { + // const element: DomElement = editorEngine.elements.selected[0]; + // instance = element.instanceId; + // root = element.oid; + // } + // let menuItems: MenuItem[][] = []; + + // if (editorEngine.isWindowSelected) { + // menuItems = [WINDOW_ITEMS, [OPEN_DEV_TOOL_ITEM]]; + // } else { + // const updatedToolItems = [ + // instance !== null && { + // label: 'View instance code', + // action: () => viewSource(instance), + // icon: , + // }, + // { + // label: `View ${instance ? 'component' : 'element'} in ${ide.displayName}`, + // disabled: !root, + // action: () => viewSource(root), + // icon: instance ? ( + // + // ) : ( + // + // ), + // }, + // ...TOOL_ITEMS, + // ].filter(Boolean) as MenuItem[]; + + // menuItems = [updatedToolItems, GROUP_ITEMS, EDITING_ITEMS]; + // } + + // setMenuItems(menuItems); + }; + + function viewSource(oid: string | null) { + editorEngine.code.viewSource(oid); + } + + return ( + + {children} + + {menuItems.map((group, groupIndex) => ( +
+ {group.map((item) => ( + + + {item.icon} + {item.label} + + {item.hotkey && {item.hotkey.readableCommand}} + + + + ))} + {groupIndex < menuItems.length - 1 && } +
+ ))} +
+
+ ); +}); \ No newline at end of file diff --git a/apps/web/src/app/project/[id]/_components/canvas/frame/top-bar.tsx b/apps/web/src/app/project/[id]/_components/canvas/frame/top-bar.tsx new file mode 100644 index 0000000000..e2851d2f02 --- /dev/null +++ b/apps/web/src/app/project/[id]/_components/canvas/frame/top-bar.tsx @@ -0,0 +1,61 @@ +import { useEditorEngine } from '@/components/store'; +import type { Frame } from '@onlook/models'; +import { observer } from 'mobx-react-lite'; + +export const TopBar = observer( + ({ + frame, + children + }: { + frame: Frame; + children?: React.ReactNode; + }) => { + const editorEngine = useEditorEngine(); + + const startMove = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const startX = e.clientX; + const startY = e.clientY; + const startPositionX = frame.position.x; + const startPositionY = frame.position.y; + + const handleMove = (e: MouseEvent) => { + const scale = editorEngine.canvas.scale; + const deltaX = (e.clientX - startX) / scale; + const deltaY = (e.clientY - startY) / scale; + + frame.position = { + x: startPositionX + deltaX, + y: startPositionY + deltaY, + }; + }; + + const endMove = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + window.removeEventListener('mousemove', handleMove); + window.removeEventListener('mouseup', endMove); + }; + + window.addEventListener('mousemove', handleMove); + window.addEventListener('mouseup', endMove); + }; + + return ( +
+ {children} +
+ ); + }, +); \ No newline at end of file diff --git a/apps/web/src/app/project/[id]/_components/canvas/frame/web-frame.tsx b/apps/web/src/app/project/[id]/_components/canvas/frame/web-frame.tsx new file mode 100644 index 0000000000..c7654618be --- /dev/null +++ b/apps/web/src/app/project/[id]/_components/canvas/frame/web-frame.tsx @@ -0,0 +1,25 @@ +import type { WebFrame } from "@onlook/models"; +import { cn } from "@onlook/ui-v4/utils"; +import { observer } from "mobx-react-lite"; + +export const WebFrameComponent = observer(({ + frame, +}: { frame: WebFrame }) => { + return ( +