Pārlūkot izejas kodu

feat(fixed-layout): fixed-layout usePlaygroundTools and demo add setInteractiveType (#806)

xiamidaxia 4 mēneši atpakaļ
vecāks
revīzija
abe7e32040

+ 25 - 2
apps/demo-fixed-layout/src/assets/icon-mouse.tsx

@@ -3,9 +3,16 @@
  * SPDX-License-Identifier: MIT
  */
 
-export function MouseIcon() {
+export function IconMouse(props: { width?: number; height?: number }) {
+  const { width, height } = props;
   return (
-    <svg width="34" height="52" viewBox="0 0 34 52" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <svg
+      width={width || 34}
+      height={height || 52}
+      viewBox="0 0 34 52"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+    >
       <path
         fillRule="evenodd"
         clipRule="evenodd"
@@ -16,3 +23,19 @@ export function MouseIcon() {
     </svg>
   );
 }
+
+export const IconMouseTool = () => (
+  <svg
+    width="1em"
+    height="1em"
+    viewBox="0 0 24 24"
+    fill="currentColor"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <path
+      fillRule="evenodd"
+      clipRule="evenodd"
+      d="M4.5 8C4.5 4.13401 7.63401 1 11.5 1H12.5C16.366 1 19.5 4.13401 19.5 8V17C19.5 20.3137 16.8137 23 13.5 23H10.5C7.18629 23 4.5 20.3137 4.5 17V8ZM11.2517 3.00606C8.60561 3.13547 6.5 5.32184 6.5 8V17C6.5 19.2091 8.29086 21 10.5 21H13.5C15.7091 21 17.5 19.2091 17.5 17V8C17.5 5.32297 15.3962 3.13732 12.7517 3.00622V5.28013C13.2606 5.54331 13.6074 6.06549 13.6074 6.66669V8.75759C13.6074 9.35879 13.2606 9.88097 12.7517 10.1441V11.4091C12.7517 11.8233 12.4159 12.1591 12.0017 12.1591C11.5875 12.1591 11.2517 11.8233 11.2517 11.4091V10.1457C10.7411 9.88298 10.3931 9.35994 10.3931 8.75759V6.66669C10.3931 6.06433 10.7411 5.5413 11.2517 5.27862V3.00606ZM12.0017 6.14397C11.7059 6.14397 11.466 6.38381 11.466 6.67968V8.74462C11.466 9.03907 11.7036 9.27804 11.9975 9.28031L12.0002 9.28032C12.0456 9.28032 12.0896 9.27482 12.1316 9.26447C12.3401 9.21256 12.5002 9.0386 12.5318 8.82287C12.5345 8.80149 12.5359 8.7797 12.5359 8.75759V6.66669C12.5359 6.64463 12.5345 6.62288 12.5318 6.60154C12.4999 6.38354 12.3368 6.20817 12.1252 6.15826C12.0856 6.14891 12.0442 6.14397 12.0017 6.14397Z"
+    ></path>
+  </svg>
+);

+ 30 - 2
apps/demo-fixed-layout/src/assets/icon-pad.tsx

@@ -3,9 +3,16 @@
  * SPDX-License-Identifier: MIT
  */
 
-export function PadIcon() {
+export function IconPad(props: { width?: number; height?: number }) {
+  const { width, height } = props;
   return (
-    <svg width="48" height="38" viewBox="0 0 48 38" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <svg
+      width={width || 48}
+      height={height || 38}
+      viewBox="0 0 48 38"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+    >
       <rect
         x="1.83317"
         y="1.49998"
@@ -26,3 +33,24 @@ export function PadIcon() {
     </svg>
   );
 }
+
+export const IconPadTool = () => (
+  <svg
+    width="1em"
+    height="1em"
+    viewBox="0 0 24 24"
+    fill="currentColor"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <path
+      fillRule="evenodd"
+      clipRule="evenodd"
+      d="M20.8549 5H3.1451C3.06496 5 3 5.06496 3 5.1451V18.8549C3 18.935 3.06496 19 3.1451 19H20.8549C20.935 19 21 18.935 21 18.8549V5.1451C21 5.06496 20.935 5 20.8549 5ZM3.1451 3C1.96039 3 1 3.96039 1 5.1451V18.8549C1 20.0396 1.96039 21 3.1451 21H20.8549C22.0396 21 23 20.0396 23 18.8549V5.1451C23 3.96039 22.0396 3 20.8549 3H3.1451Z"
+    ></path>
+    <path
+      fillRule="evenodd"
+      clipRule="evenodd"
+      d="M6.99991 16C6.99991 15.4477 7.44762 15 7.99991 15H15.9999C16.5522 15 16.9999 15.4477 16.9999 16C16.9999 16.5523 16.5522 17 15.9999 17H7.99991C7.44762 17 6.99991 16.5523 6.99991 16Z"
+    ></path>
+  </svg>
+);

+ 1 - 0
apps/demo-fixed-layout/src/components/base-node/styles.tsx

@@ -17,6 +17,7 @@ export const BaseNodeStyle = styled.div`
   justify-content: center;
   position: relative;
   width: 360px;
+  cursor: default;
   &.activated {
     border: 1px solid #82a7fc;
   }

+ 2 - 0
apps/demo-fixed-layout/src/components/tools/index.tsx

@@ -17,6 +17,7 @@ import { Run } from './run';
 import { Readonly } from './readonly';
 import { MinimapSwitch } from './minimap-switch';
 import { Minimap } from './minimap';
+import { Interactive } from './interactive';
 import { FitView } from './fit-view';
 
 export const DemoTools = () => {
@@ -33,6 +34,7 @@ export const DemoTools = () => {
   return (
     <ToolContainer className="fixed-demo-tools">
       <ToolSection>
+        <Interactive />
         <SwitchVertical />
         <ZoomSelect />
         <FitView fitView={tools.fitView} />

+ 95 - 0
apps/demo-fixed-layout/src/components/tools/interactive.tsx

@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { useEffect, useState } from 'react';
+
+import { usePlaygroundTools, PlaygroundInteractiveType } from '@flowgram.ai/fixed-layout-editor';
+import { Tooltip, Popover } from '@douyinfe/semi-ui';
+
+import { MousePadSelector } from './mouse-pad-selector';
+
+export const CACHE_KEY = 'workflow_prefer_interactive_type';
+export const IS_MAC_OS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);
+
+export const getPreferInteractiveType = () => {
+  const data = localStorage.getItem(CACHE_KEY) as string;
+  if (data && [InteractiveType.Mouse, InteractiveType.Pad].includes(data as InteractiveType)) {
+    return data;
+  }
+  return IS_MAC_OS ? InteractiveType.Pad : InteractiveType.Mouse;
+};
+
+export const setPreferInteractiveType = (type: InteractiveType) => {
+  localStorage.setItem(CACHE_KEY, type);
+};
+
+export enum InteractiveType {
+  Mouse = 'MOUSE',
+  Pad = 'PAD',
+}
+
+export const Interactive = () => {
+  const tools = usePlaygroundTools();
+  const [visible, setVisible] = useState(false);
+
+  const [interactiveType, setInteractiveType] = useState<InteractiveType>(
+    () => getPreferInteractiveType() as InteractiveType
+  );
+
+  const [showInteractivePanel, setShowInteractivePanel] = useState(false);
+
+  const mousePadTooltip =
+    interactiveType === InteractiveType.Mouse ? 'Mouse-Friendly' : 'Touchpad-Friendly';
+
+  useEffect(() => {
+    // read from localStorage
+    const preferInteractiveType = getPreferInteractiveType();
+    tools.setInteractiveType(preferInteractiveType as PlaygroundInteractiveType);
+  }, []);
+
+  const handleClose = () => {
+    setVisible(false);
+  };
+
+  return (
+    <Popover trigger="custom" position="top" visible={visible} onClickOutSide={handleClose}>
+      <Tooltip
+        content={mousePadTooltip}
+        style={{ display: showInteractivePanel ? 'none' : 'block' }}
+      >
+        <div className="workflow-toolbar-interactive">
+          <MousePadSelector
+            value={interactiveType}
+            onChange={(value) => {
+              setInteractiveType(value);
+              setPreferInteractiveType(value);
+              tools.setInteractiveType(value);
+            }}
+            onPopupVisibleChange={setShowInteractivePanel}
+            containerStyle={{
+              border: 'none',
+              height: '32px',
+              width: '32px',
+              justifyContent: 'center',
+              alignItems: 'center',
+              gap: '2px',
+              padding: '4px',
+              borderRadius: 'var(--small, 6px)',
+            }}
+            iconStyle={{
+              margin: '0',
+              width: '16px',
+              height: '16px',
+            }}
+            arrowStyle={{
+              width: '12px',
+              height: '12px',
+            }}
+          />
+        </div>
+      </Tooltip>
+    </Popover>
+  );
+};

+ 117 - 0
apps/demo-fixed-layout/src/components/tools/mouse-pad-selector.less

@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+/* stylelint-disable no-descending-specificity */
+/* stylelint-disable selector-class-pattern */
+.ui-mouse-pad-selector {
+  position: relative;
+
+  display: flex;
+  align-items: center;
+
+  box-sizing: border-box;
+  width: 68px;
+  height: 32px;
+  padding: 8px 12px;
+
+  border: 1px solid rgba(29, 28, 35, 8%);
+  border-radius: 8px;
+
+  &-icon {
+    height: 20px;
+    margin-right: 12px;
+  }
+
+  &-arrow {
+    height: 16px;
+    font-size: 12px;
+  }
+
+  &-popover {
+    padding: 16px;
+
+    &-options {
+      display: flex;
+      gap: 12px;
+      margin-top: 12px;
+    }
+
+    .mouse-pad-option {
+      box-sizing: border-box;
+      width: 220px;
+      padding-bottom: 20px;
+
+      text-align: center;
+
+      background: var(--coz-mg-card, #FFF);
+      border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));
+      border-radius: var(--default, 8px);
+
+      &-icon {
+        padding-top: 26px;
+      }
+
+      &-title {
+        padding-top: 8px;
+      }
+
+      &-subTitle {
+        padding: 4px 12px 0;
+      }
+
+      &-icon-selected {
+        color: rgb(19 0 221);
+      }
+
+      &-title-selected {
+        color: var(--coz-fg-hglt, #4E40E5);
+      }
+
+      &-subTitle-selected {
+        color: var(--coz-fg-hglt, #4E40E5);
+      }
+
+      &-selected {
+        cursor: pointer;
+        background-color: var(--coz-mg-hglt, rgba(186, 192, 255, 20%));
+        border: 1px solid var(--coz-stroke-hglt, #4E40E5);
+        border-radius: var(--default, 8px);
+      }
+
+      &:hover:not(&-selected) {
+        cursor: pointer;
+
+        background-color: var(--coz-mg-card-hovered, #FFF);
+        border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));
+        border-radius: var(--default, 8px);
+        box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 16%), 0 16px 48px 0 rgba(0, 0, 0, 8%);
+      }
+
+      &:active:not(&-selected) {
+        background-color: rgba(46, 46, 56, 12%);
+      }
+
+      &:last-of-type {
+        padding-top: 13px;
+      }
+    }
+  }
+
+  &:hover {
+    cursor: pointer;
+    background-color: rgba(46, 46, 56, 8%);
+    border-color: rgba(77, 83, 232, 100%);
+  }
+
+  &:active,
+  &:focus {
+    background-color: rgba(46, 46, 56, 12%);
+    border-color: rgba(77, 83, 232, 100%);
+  }
+
+  &-active {
+    border-color: rgba(77, 83, 232, 100%);
+  }
+}

+ 122 - 0
apps/demo-fixed-layout/src/components/tools/mouse-pad-selector.tsx

@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import React, { type CSSProperties, useState } from 'react';
+
+import { Popover, Typography } from '@douyinfe/semi-ui';
+
+import { IconPad, IconPadTool } from '../../assets/icon-pad';
+import { IconMouse, IconMouseTool } from '../../assets/icon-mouse';
+
+import './mouse-pad-selector.less';
+
+const { Title, Paragraph } = Typography;
+
+export enum InteractiveType {
+  Mouse = 'MOUSE',
+  Pad = 'PAD',
+}
+
+export interface MousePadSelectorProps {
+  value: InteractiveType;
+  onChange: (value: InteractiveType) => void;
+  onPopupVisibleChange?: (visible: boolean) => void;
+  containerStyle?: CSSProperties;
+  iconStyle?: CSSProperties;
+  arrowStyle?: CSSProperties;
+}
+
+const InteractiveItem: React.FC<{
+  title: string;
+  subTitle: string;
+  icon: React.ReactNode;
+  value: InteractiveType;
+  selected: boolean;
+  onChange: (value: InteractiveType) => void;
+}> = ({ title, subTitle, icon, onChange, value, selected }) => (
+  <div
+    className={`mouse-pad-option ${selected ? 'mouse-pad-option-selected' : ''}`}
+    onClick={() => onChange(value)}
+  >
+    <div className={`mouse-pad-option-icon ${selected ? 'mouse-pad-option-icon-selected' : ''}`}>
+      {icon}
+    </div>
+    <Title
+      heading={6}
+      className={`mouse-pad-option-title ${selected ? 'mouse-pad-option-title-selected' : ''}`}
+    >
+      {title}
+    </Title>
+    <Paragraph
+      type="tertiary"
+      className={`mouse-pad-option-subTitle ${
+        selected ? 'mouse-pad-option-subTitle-selected' : ''
+      }`}
+    >
+      {subTitle}
+    </Paragraph>
+  </div>
+);
+
+export const MousePadSelector: React.FC<
+  MousePadSelectorProps & React.RefAttributes<HTMLDivElement>
+> = ({ value, onChange, onPopupVisibleChange, containerStyle, iconStyle, arrowStyle }) => {
+  const isMouse = value === InteractiveType.Mouse;
+  const [visible, setVisible] = useState(false);
+
+  return (
+    <Popover
+      trigger="custom"
+      position="topLeft"
+      closeOnEsc
+      visible={visible}
+      onVisibleChange={(v) => {
+        onPopupVisibleChange?.(v);
+      }}
+      onClickOutSide={() => {
+        setVisible(false);
+      }}
+      spacing={20}
+      content={
+        <div className={'ui-mouse-pad-selector-popover'}>
+          <Typography.Title heading={4}>{'Interaction mode'}</Typography.Title>
+          <div className={'ui-mouse-pad-selector-popover-options'}>
+            <InteractiveItem
+              title={'Mouse-Friendly'}
+              subTitle={'Drag the canvas with the left mouse button, zoom with the scroll wheel.'}
+              value={InteractiveType.Mouse}
+              selected={value === InteractiveType.Mouse}
+              icon={<IconMouse />}
+              onChange={onChange}
+            />
+
+            <InteractiveItem
+              title={'Touchpad-Friendly'}
+              subTitle={
+                'Drag with two fingers moving in the same direction, zoom by pinching or spreading two fingers.'
+              }
+              value={InteractiveType.Pad}
+              selected={value === InteractiveType.Pad}
+              icon={<IconPad />}
+              onChange={onChange}
+            />
+          </div>
+        </div>
+      }
+    >
+      <div
+        className={`ui-mouse-pad-selector ${visible ? 'ui-mouse-pad-selector-active' : ''}`}
+        onClick={() => {
+          setVisible(!visible);
+        }}
+        style={containerStyle}
+      >
+        <div className={'ui-mouse-pad-selector-icon'} style={iconStyle}>
+          {isMouse ? <IconMouseTool /> : <IconPadTool />}
+        </div>
+      </div>
+    </Popover>
+  );
+};

+ 1 - 1
apps/demo-fixed-layout/src/form-components/form-header/index.tsx

@@ -82,7 +82,7 @@ export function FormHeader() {
       onMouseDown={(e) => {
         // trigger drag node
         startDrag(e);
-        // e.stopPropagation();
+        e.stopPropagation();
       }}
     >
       {getIcon(node)}

+ 1 - 1
apps/demo-fixed-layout/src/form-components/form-header/styles.tsx

@@ -16,8 +16,8 @@ export const Header = styled.div`
 
   background: linear-gradient(#f2f2ff 0%, rgb(251, 251, 251) 100%);
   overflow: hidden;
-
   padding: 8px;
+  cursor: move;
 `;
 
 export const Title = styled.div`

+ 1 - 0
apps/demo-fixed-layout/src/hooks/use-editor-props.ts

@@ -45,6 +45,7 @@ export function useEditorProps(
        * Canvas-related configurations
        */
       playground: {
+        ineractiveType: 'MOUSE',
         /**
          * Prevent Mac browser gestures from turning pages
          * 阻止 mac 浏览器手势翻页

+ 0 - 3
apps/demo-free-layout/src/components/tools/interactive.tsx

@@ -14,7 +14,6 @@ import { Tooltip, Popover } from '@douyinfe/semi-ui';
 import { MousePadSelector } from './mouse-pad-selector';
 
 export const CACHE_KEY = 'workflow_prefer_interactive_type';
-export const SHOW_KEY = 'show_workflow_interactive_type_guide';
 export const IS_MAC_OS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);
 
 export const getPreferInteractiveType = () => {
@@ -48,8 +47,6 @@ export const Interactive = () => {
     interactiveType === InteractiveType.Mouse ? 'Mouse-Friendly' : 'Touchpad-Friendly';
 
   useEffect(() => {
-    tools.setMouseScrollDelta((zoom) => zoom / 20);
-
     // read from localStorage
     const preferInteractiveType = getPreferInteractiveType();
     tools.setInteractiveType(preferInteractiveType as IdeInteractiveType);

+ 1 - 1
packages/canvas-engine/core/src/core/layer/config/playground-config-entity.ts

@@ -56,7 +56,7 @@ export interface PlaygroundConfigRevealOpts {
 export const SCALE_WIDTH = 0
 
 /** 鼠标缩放 delta */
-export const MOUSE_SCROLL_DELTA = 0.05;
+export const MOUSE_SCROLL_DELTA = (zoom: number) => zoom / 20;
 export type PlaygroundScrollLimitFn = (scroll: { scrollX: number; scrollY: number }) => {
   scrollX: number
   scrollY: number

+ 1 - 2
packages/canvas-engine/core/src/core/layer/playground-layer.ts

@@ -19,7 +19,6 @@ import {
   EditorStateConfigEntity,
   PlaygroundConfigEntity,
   type PlaygroundConfigEntityData,
-  MOUSE_SCROLL_DELTA,
 } from './config';
 
 /**
@@ -437,7 +436,7 @@ export class PlaygroundLayer extends Layer<PlaygroundLayerOptions> {
     if (typeof mouseScrollDelta === 'function') {
       return mouseScrollDelta(zoom);
     }
-    return mouseScrollDelta ?? MOUSE_SCROLL_DELTA;
+    return mouseScrollDelta!;
   }
 
   /**

+ 22 - 1
packages/client/fixed-layout-editor/src/hooks/use-playground-tools.ts

@@ -7,7 +7,13 @@ import { useCallback, useEffect, useState } from 'react';
 
 import { DisposableCollection } from '@flowgram.ai/utils';
 import { HistoryService } from '@flowgram.ai/history';
-import { FlowDocument, FlowLayoutDefault, FlowNodeRenderData } from '@flowgram.ai/editor';
+import {
+  FlowDocument,
+  FlowLayoutDefault,
+  FlowNodeRenderData,
+  PlaygroundInteractiveType,
+  EditorState,
+} from '@flowgram.ai/editor';
 import { usePlayground, usePlaygroundContainer, useService } from '@flowgram.ai/editor';
 
 export interface PlaygroundToolsPropsType {
@@ -52,6 +58,9 @@ export interface PlaygroundTools {
    */
   changeLayout: (layout?: FlowLayoutDefault) => void;
 
+  /** 交互模式:鼠标 or 触控板 */
+  interactiveType: PlaygroundInteractiveType;
+  setInteractiveType: (type: PlaygroundInteractiveType) => void;
   /**
    * 是否可 redo
    */
@@ -78,6 +87,7 @@ export function usePlaygroundTools(props?: PlaygroundToolsPropsType): Playground
     ? container.get(HistoryService)
     : undefined;
   const doc = useService<FlowDocument>(FlowDocument);
+  const [interactiveType, setInteractiveType] = useState<PlaygroundInteractiveType>('PAD');
 
   const [zoom, setZoom] = useState(1);
   const [currentLayout, updateLayout] = useState(doc.layout);
@@ -142,6 +152,15 @@ export function usePlaygroundTools(props?: PlaygroundToolsPropsType): Playground
   const handleUndo = useCallback(() => historyService?.undo(), [historyService]);
   const handleRedo = useCallback(() => historyService?.redo(), [historyService]);
 
+  function handleUpdateInteractiveType(interactiveType: PlaygroundInteractiveType) {
+    if (interactiveType === 'MOUSE') {
+      playground.editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);
+    } else if (interactiveType === 'PAD') {
+      playground.editorState.changeState(EditorState.STATE_SELECT.id);
+    }
+    setInteractiveType(interactiveType);
+  }
+
   useEffect(() => {
     const dispose = new DisposableCollection();
     if (playground) {
@@ -178,5 +197,7 @@ export function usePlaygroundTools(props?: PlaygroundToolsPropsType): Playground
     canUndo,
     undo: handleUndo,
     redo: handleRedo,
+    interactiveType,
+    setInteractiveType: handleUpdateInteractiveType,
   };
 }