diff --git a/blocksuite/blocks/src/effects.ts b/blocksuite/blocks/src/effects.ts index ff6a2b401..ce0f22156 100644 --- a/blocksuite/blocks/src/effects.ts +++ b/blocksuite/blocks/src/effects.ts @@ -162,7 +162,6 @@ import { AffineInnerModalWidget, AffineModalWidget, AffinePageDraggingAreaWidget, - AffinePieMenuWidget, AffineSlashMenuWidget, AffineSurfaceRefToolbar, EdgelessCopilotToolbarEntry, @@ -210,13 +209,6 @@ import { Loader } from './root-block/widgets/linked-doc/import-doc/loader'; import { AffineCustomModal } from './root-block/widgets/modal/custom-modal.js'; import { AFFINE_MODAL_WIDGET } from './root-block/widgets/modal/modal.js'; import { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from './root-block/widgets/page-dragging-area/page-dragging-area.js'; -import { PieNodeCenter } from './root-block/widgets/pie-menu/components/pie-node-center.js'; -import { PieNodeChild } from './root-block/widgets/pie-menu/components/pie-node-child.js'; -import { PieNodeContent } from './root-block/widgets/pie-menu/components/pie-node-content.js'; -import { PieCenterRotator } from './root-block/widgets/pie-menu/components/rotator.js'; -import { AFFINE_PIE_MENU_WIDGET } from './root-block/widgets/pie-menu/index.js'; -import { PieMenu } from './root-block/widgets/pie-menu/menu.js'; -import { PieNode } from './root-block/widgets/pie-menu/node.js'; import { AFFINE_SLASH_MENU_WIDGET } from './root-block/widgets/slash-menu/index.js'; import { InnerSlashMenu, @@ -327,10 +319,6 @@ export function effects() { customElements.define('affine-custom-modal', AffineCustomModal); customElements.define('affine-database', DatabaseBlockComponent); customElements.define('affine-surface-ref', SurfaceRefBlockComponent); - customElements.define('pie-node-child', PieNodeChild); - customElements.define('pie-node-content', PieNodeContent); - customElements.define('pie-node-center', PieNodeCenter); - customElements.define('pie-center-rotator', PieCenterRotator); customElements.define('affine-slash-menu', SlashMenu); customElements.define('inner-slash-menu', InnerSlashMenu); customElements.define('generating-placeholder', GeneratingPlaceholder); @@ -347,7 +335,6 @@ export function effects() { EdgelessDraggingAreaRectWidget ); customElements.define('icon-button', IconButton); - customElements.define('affine-pie-menu', PieMenu); customElements.define('loader-element', Loader); customElements.define('edgeless-brush-menu', EdgelessBrushMenu); customElements.define( @@ -359,7 +346,6 @@ export function effects() { 'edgeless-connector-tool-button', EdgelessConnectorToolButton ); - customElements.define('affine-pie-node', PieNode); customElements.define( 'edgeless-default-tool-button', EdgelessDefaultToolButton @@ -501,7 +487,6 @@ export function effects() { AffinePageDraggingAreaWidget ); customElements.define(AFFINE_DRAG_HANDLE_WIDGET, AffineDragHandleWidget); - customElements.define(AFFINE_PIE_MENU_WIDGET, AffinePieMenuWidget); customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget); customElements.define(AFFINE_IMAGE_TOOLBAR_WIDGET, AffineImageToolbarWidget); diff --git a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-spec.ts b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-spec.ts index e792d10e7..c05929a0b 100644 --- a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-spec.ts +++ b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-spec.ts @@ -33,7 +33,6 @@ import { AFFINE_FRAME_TITLE_WIDGET } from '../widgets/frame-title/index.js'; import { AFFINE_INNER_MODAL_WIDGET } from '../widgets/inner-modal/inner-modal.js'; import { AFFINE_LINKED_DOC_WIDGET } from '../widgets/linked-doc/index.js'; import { AFFINE_MODAL_WIDGET } from '../widgets/modal/modal.js'; -import { AFFINE_PIE_MENU_WIDGET } from '../widgets/pie-menu/index.js'; import { AFFINE_SLASH_MENU_WIDGET } from '../widgets/slash-menu/index.js'; import { AFFINE_VIEWPORT_OVERLAY_WIDGET } from '../widgets/viewport-overlay/viewport-overlay.js'; import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js'; @@ -46,7 +45,6 @@ import { EdgelessRootService } from './edgeless-root-service.js'; export const edgelessRootWidgetViewMap = { [AFFINE_MODAL_WIDGET]: literal`${unsafeStatic(AFFINE_MODAL_WIDGET)}`, [AFFINE_INNER_MODAL_WIDGET]: literal`${unsafeStatic(AFFINE_INNER_MODAL_WIDGET)}`, - [AFFINE_PIE_MENU_WIDGET]: literal`${unsafeStatic(AFFINE_PIE_MENU_WIDGET)}`, [AFFINE_SLASH_MENU_WIDGET]: literal`${unsafeStatic( AFFINE_SLASH_MENU_WIDGET )}`, diff --git a/blocksuite/blocks/src/root-block/types.ts b/blocksuite/blocks/src/root-block/types.ts index e5da01892..ebf610d7d 100644 --- a/blocksuite/blocks/src/root-block/types.ts +++ b/blocksuite/blocks/src/root-block/types.ts @@ -13,8 +13,6 @@ import type { AFFINE_INNER_MODAL_WIDGET } from './widgets/inner-modal/inner-moda import type { AFFINE_LINKED_DOC_WIDGET } from './widgets/linked-doc/index.js'; import type { AFFINE_MODAL_WIDGET } from './widgets/modal/modal.js'; import type { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from './widgets/page-dragging-area/page-dragging-area.js'; -import type { AFFINE_PIE_MENU_ID_EDGELESS_TOOLS } from './widgets/pie-menu/config.js'; -import type { AFFINE_PIE_MENU_WIDGET } from './widgets/pie-menu/index.js'; import type { AFFINE_SLASH_MENU_WIDGET } from './widgets/slash-menu/index.js'; import type { AFFINE_VIEWPORT_OVERLAY_WIDGET } from './widgets/viewport-overlay/viewport-overlay.js'; @@ -34,7 +32,6 @@ export type PageRootBlockWidgetName = export type EdgelessRootBlockWidgetName = | typeof AFFINE_MODAL_WIDGET | typeof AFFINE_INNER_MODAL_WIDGET - | typeof AFFINE_PIE_MENU_WIDGET | typeof AFFINE_SLASH_MENU_WIDGET | typeof AFFINE_LINKED_DOC_WIDGET | typeof AFFINE_DRAG_HANDLE_WIDGET @@ -50,5 +47,3 @@ export type EdgelessRootBlockWidgetName = export type RootBlockComponent = | PageRootBlockComponent | EdgelessRootBlockComponent; - -export type PieMenuId = typeof AFFINE_PIE_MENU_ID_EDGELESS_TOOLS; diff --git a/blocksuite/blocks/src/root-block/widgets/index.ts b/blocksuite/blocks/src/root-block/widgets/index.ts index 389fd57d5..3227b96c6 100644 --- a/blocksuite/blocks/src/root-block/widgets/index.ts +++ b/blocksuite/blocks/src/root-block/widgets/index.ts @@ -46,7 +46,6 @@ export { export { AffineLinkedDocWidget } from './linked-doc/index.js'; export { AffineModalWidget } from './modal/modal.js'; export { AffinePageDraggingAreaWidget } from './page-dragging-area/page-dragging-area.js'; -export { AffinePieMenuWidget } from './pie-menu/index.js'; export { type AffineSlashMenuActionItem, type AffineSlashMenuContext, diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/base.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/base.ts deleted file mode 100644 index 38dfb28d9..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/base.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { TemplateResult } from 'lit'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import type { PieMenuId } from '../../types.js'; -import type { AffinePieMenuWidget } from './index.js'; -import type { PieMenu } from './menu.js'; -import type { PieNode } from './node.js'; - -export interface PieMenuSchema { - id: PieMenuId; - - label: string; - - root: PieRootNodeModel; - - trigger: (props: { - keyEvent: KeyboardEvent; - rootComponent: EdgelessRootBlockComponent; - }) => boolean; -} - -export type IconGetter = (ctx: PieMenuContext) => TemplateResult; -export type DisabledGetter = (ctx: PieMenuContext) => boolean; -export interface PieBaseNodeModel { - type: 'root' | 'command' | 'submenu' | 'toggle' | 'color'; - - label: string; - - icon?: IconGetter | TemplateResult; - - angle?: number; - - startAngle?: number; - - endAngle?: number; - - disabled?: boolean | DisabledGetter; -} - -// A menu can only have one root node -export interface PieRootNodeModel extends PieBaseNodeModel { - type: 'root'; - children: Array; -} - -export type PieMenuContext = { - rootComponent: EdgelessRootBlockComponent; - menu: PieMenu; - widgetComponent: AffinePieMenuWidget; - node: PieNode; -}; -export type ActionFunction = (ctx: PieMenuContext) => void; - -// Nodes which can perform a given action -export interface PieCommandNodeModel extends PieBaseNodeModel { - type: 'command'; - action: ActionFunction; -} - -// Open a submenu -export interface PieSubmenuNodeModel extends PieBaseNodeModel { - type: 'submenu'; - role: 'default' | 'color-picker' | 'command'; - action?: ActionFunction; - children: Array; - openOnHover?: boolean; - timeoutOverride?: number; -} - -export interface PieColorNodeModel extends PieBaseNodeModel { - type: 'color'; - color: string; - hollowCircle: boolean; - text?: string; - onChange: (color: string, ctx: PieMenuContext) => void; -} - -export type IPieNodeWithAction = - | PieCommandNodeModel - | (PieSubmenuNodeModel & { role: 'command'; action: ActionFunction }); - -export type PieNonRootNode = - | PieCommandNodeModel - | PieColorNodeModel - | PieSubmenuNodeModel; - -export type PieNodeModel = PieRootNodeModel | PieNonRootNode; diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/components/pie-node-center.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/components/pie-node-center.ts deleted file mode 100644 index 6ecd6e0fd..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/components/pie-node-center.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { css, html, LitElement } from 'lit'; -import { property } from 'lit/decorators.js'; -import { styleMap } from 'lit/directives/style-map.js'; - -import { PieNode } from '../node.js'; - -const styles = css` - .pie-parent-node-container { - position: absolute; - list-style-type: none; - } - - .pie-node.center { - width: 6rem; - height: 6rem; - padding: 0.4rem; - } - - .pie-node.center[active='true'] .node-content > svg, - .pie-node.center[active='true'] .node-content > .color-unit, - .pie-node.center[active='true'] .node-content > .color-unit > svg { - width: 2rem !important; - height: 2rem !important; - } - - .pie-node.center[active='false'] { - width: 3rem; - height: 3rem; - opacity: 0.6; - } -`; - -export class PieNodeCenter extends LitElement { - static override styles = [PieNode.styles, styles]; - - protected override render() { - const [x, y] = this.node.position; - - const styles = { - transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`, - }; - - return html` -
-
- - - -
- -
- `; - } - - @property({ attribute: false }) - accessor hoveredNode!: PieNode | null; - - @property({ attribute: false }) - accessor isActive!: boolean; - - @property({ attribute: false }) - accessor node!: PieNode; - - @property({ attribute: false }) - accessor onMouseEnter!: (ev: MouseEvent) => void; - - @property({ attribute: false }) - accessor rotatorAngle: number | null = null; -} - -declare global { - interface HTMLElementTagNameMap { - 'pie-node-center': PieNodeCenter; - } -} diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/components/pie-node-child.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/components/pie-node-child.ts deleted file mode 100644 index 2e138f452..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/components/pie-node-child.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { css, html, LitElement } from 'lit'; -import { property } from 'lit/decorators.js'; -import { styleMap } from 'lit/directives/style-map.js'; - -import { PieNode } from '../node.js'; - -const styles = css` - .pie-node.child { - width: 3rem; - height: 3rem; - padding: 0.6rem; - animation: my-anim 250ms cubic-bezier(0.775, 1.325, 0.535, 1); - } - - .pie-node.child.node-color { - width: 0.7rem; - height: 0.7rem; - } - - .pie-node.child:not(.node-color)::after { - content: attr(index); - color: var(--affine-text-secondary-color); - position: absolute; - font-size: 8px; - bottom: 10%; - right: 30%; - } - - .pie-node.child[hovering='true'] { - border-color: var(--affine-primary-color); - background-color: var(--affine-hover-color-filled); - scale: 1.06; - } - - .pie-node.child.node-submenu::before { - content: ''; - position: absolute; - top: 50%; - right: 10%; - transform: translateY(-50%); - width: 5px; - height: 5px; - background-color: var(--affine-primary-color); - border-radius: 50%; - } -`; - -export class PieNodeChild extends LitElement { - static override styles = [PieNode.styles, styles]; - - protected override render() { - const { model, position } = this.node; - - const [x, y] = position; - - const styles = { - top: '50%', - left: '50%', - transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`, - visibility: this.visible ? 'visible' : 'hidden', - }; - - return html`
  • - - -
  • `; - } - - @property({ attribute: false }) - accessor hovering!: boolean; - - @property({ attribute: false }) - accessor node!: PieNode; - - @property({ attribute: false }) - accessor onClick!: (ev: MouseEvent) => void; - - @property({ attribute: false }) - accessor visible!: boolean; -} - -declare global { - interface HTMLElementTagNameMap { - 'pie-node-child': PieNodeChild; - } -} diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/components/pie-node-content.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/components/pie-node-content.ts deleted file mode 100644 index 70c04b00c..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/components/pie-node-content.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { assertEquals } from '@blocksuite/global/utils'; -import { css, html, LitElement, type PropertyValues } from 'lit'; -import { property, query } from 'lit/decorators.js'; - -import { ColorUnit } from '../../../edgeless/components/panel/color-panel.js'; -import type { PieNode } from '../node.js'; -import { isSubmenuNode } from '../utils.js'; - -const styles = css` - .node-content > svg { - width: 24px; - height: 24px; - } - - .node-content.center[active='true'] > svg, - .node-content.center[active='true'] > .color-unit, - .node-content.center[active='true'] > .color-unit > svg { - width: 2rem !important; - height: 2rem !important; - } -`; - -export class PieNodeContent extends LitElement { - static override styles = styles; - - private _renderCenterNodeContent() { - if (isSubmenuNode(this.node.model) && !this.isActive) { - return this._renderChildNodeContent(); - } - - const { menu, model } = this.node; - const isActiveNode = menu.isActiveNode(this.node); - const hoveredNode = this.hoveredNode; - - if ( - this.isActive && - isSubmenuNode(model) && - model.role === 'color-picker' - ) { - if (!hoveredNode) return this.node.icon; - - assertEquals( - hoveredNode.model.type, - 'color', - 'IPieSubMenuNode.role with color-picker should have children of type color' - ); - const { color, hollowCircle } = hoveredNode.model; - return ColorUnit(color, { hollowCircle }); - } - - const { label } = model; - const centerLabelOrIcon = this.node.icon ?? label; - - return isActiveNode - ? hoveredNode - ? hoveredNode.model.label - : centerLabelOrIcon - : centerLabelOrIcon; - } - - private _renderChildNodeContent() { - return this.node.icon; - } - - protected override render() { - const content = this.node.isCenterNode() - ? this._renderCenterNodeContent() - : this._renderChildNodeContent(); - - return html` -
    - ${content} -
    - `; - } - - protected override updated(changedProperties: PropertyValues): void { - super.updated(changedProperties); - - if ( - !changedProperties.has('hoveredNode') || - !this._nodeContentElement || - !this.isActive - ) - return; - const fadeIn = [ - { - opacity: 0, - }, - { opacity: 1 }, - ]; - - this._nodeContentElement.animate(fadeIn, { - duration: 250, - easing: 'cubic-bezier(0.775, 1.325, 0.535, 1)', - fill: 'forwards' as const, - }); - } - - @query('.node-content') - private accessor _nodeContentElement!: HTMLDivElement; - - @property({ attribute: false }) - accessor hoveredNode!: PieNode | null; - - @property({ attribute: false }) - accessor isActive!: boolean; - - @property({ attribute: false }) - accessor node!: PieNode; -} - -declare global { - interface HTMLElementTagNameMap { - 'pie-node-content': PieNodeContent; - } -} diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/components/rotator.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/components/rotator.ts deleted file mode 100644 index 3369cd748..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/components/rotator.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { CommonUtils } from '@blocksuite/affine-block-surface'; -import { css, html, LitElement, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; -import { styleMap } from 'lit/directives/style-map.js'; - -import { getPosition } from '../utils.js'; - -const styles = css` - .rotator { - position: absolute; - background: var(--affine-background-overlay-panel-color); - box-shadow: var(--affine-shadow-2); - border: 2px solid var(--affine-primary-color); - border-radius: 50%; - width: 7px; - height: 7px; - top: 50%; - left: 50%; - } -`; - -export class PieCenterRotator extends LitElement { - static override styles = styles; - - protected override render() { - if (!this.isActive || this.angle === null) return nothing; - - const [x, y] = getPosition(CommonUtils.toRadian(this.angle), [45, 45]); - - const styles = { - transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`, - }; - - return html``; - } - - @property({ attribute: false }) - accessor angle: number | null = null; - - @property({ attribute: false }) - accessor isActive!: boolean; -} - -declare global { - interface HTMLElementTagNameMap { - 'pie-center-rotator': PieCenterRotator; - } -} diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/config.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/config.ts deleted file mode 100644 index b2c043bb8..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/config.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { - ConnectorCWithArrowIcon, - ConnectorIcon, - ConnectorLWithArrowIcon, - ConnectorXWithArrowIcon, - DiamondIcon, - EdgelessEraserLightIcon, - EdgelessGeneralShapeIcon, - EdgelessPenLightIcon, - EllipseIcon, - FrameIcon, - FrameNavigatorIcon, - GeneralStyleIcon, - NoteIcon, - ScribbledDiamondIcon, - ScribbledEllipseIcon, - ScribbledSquareIcon, - ScribbledStyleIcon, - ScribbledTriangleIcon, - SelectIcon, - SquareIcon, - ToolsIcon, - TriangleIcon, - ViewBarIcon, -} from '@blocksuite/affine-components/icons'; -import { - ConnectorMode, - LINE_COLORS, - SHAPE_FILL_COLORS, - SHAPE_STROKE_COLORS, - ShapeStyle, - ShapeType, -} from '@blocksuite/affine-model'; -import { - EditPropsStore, - type LastProps, -} from '@blocksuite/affine-shared/services'; -import { isControlledKeyboardEvent } from '@blocksuite/affine-shared/utils'; -import { html } from 'lit'; -import { styleMap } from 'lit/directives/style-map.js'; - -import { - DEFAULT_NOTE_CHILD_FLAVOUR, - DEFAULT_NOTE_CHILD_TYPE, - DEFAULT_NOTE_TIP, -} from '../../edgeless/utils/consts.js'; -import type { PieMenuContext } from './base.js'; -import { PieMenuBuilder } from './pie-builder.js'; -import { - getActiveConnectorStrokeColor, - getActiveShapeColor, - setEdgelessToolAction, - updateShapeOverlay, -} from './utils.js'; - -//---------------------------------------------------------- -// EDGELESS TOOLS PIE MENU SCHEMA -//---------------------------------------------------------- - -export const AFFINE_PIE_MENU_ID_EDGELESS_TOOLS = 'affine:pie:edgeless:tools'; - -const pie = new PieMenuBuilder({ - id: AFFINE_PIE_MENU_ID_EDGELESS_TOOLS, - label: 'Tools', - icon: ToolsIcon, - trigger: ({ keyEvent: ev, rootComponent }) => { - if (isControlledKeyboardEvent(ev)) return false; - const isEditing = rootComponent.service.selection.editing; - - return ev.key === 'q' && !isEditing; - }, -}); - -pie.expandableCommand({ - label: 'Pen', - icon: EdgelessPenLightIcon, - action: setEdgelessToolAction(tool => tool.setTool('brush')), - submenus: pie => { - pie.colorPicker({ - label: 'Pen Color', - active: getActiveConnectorStrokeColor, - onChange: (color: string, { rootComponent }: PieMenuContext) => { - rootComponent.std.get(EditPropsStore).recordLastProps('brush', { - color: color as LastProps['brush']['color'], - }); - }, - colors: LINE_COLORS.map(color => ({ color })), - }); - }, -}); - -pie.command({ - label: 'Eraser', - icon: EdgelessEraserLightIcon, - action: setEdgelessToolAction(tool => tool.setTool('eraser')), -}); - -pie.command({ - label: 'Frame', - icon: FrameIcon, - action: setEdgelessToolAction(tool => tool.setTool('frame')), -}); - -pie.command({ - label: 'Select', - icon: SelectIcon, - action: setEdgelessToolAction(tool => tool.setTool('default')), -}); - -pie.command({ - label: 'Note', - icon: NoteIcon, - action: setEdgelessToolAction(tool => - tool.setTool('affine:note', { - childFlavour: DEFAULT_NOTE_CHILD_FLAVOUR, - childType: DEFAULT_NOTE_CHILD_TYPE, - tip: DEFAULT_NOTE_TIP, - }) - ), -}); - -pie.command({ - label: 'Reset Zoom', - icon: ViewBarIcon, - action: ({ rootComponent }) => { - rootComponent.service.zoomToFit(); - }, -}); - -pie.command({ - label: 'Present', - icon: ({ rootComponent }) => { - const { type } = rootComponent.gfx.tool.currentToolOption$.peek(); - if (type === 'frameNavigator') { - return html` - Stop - `; - } - - return FrameNavigatorIcon; - }, - action: ({ rootComponent }) => { - const toolName = rootComponent.gfx.tool.currentToolName$.peek(); - if (toolName === 'frameNavigator') { - rootComponent.gfx.tool.setTool('default'); - - if (document.fullscreenElement) { - document.exitFullscreen().catch(console.error); - } - - return; - } - - rootComponent.gfx.tool.setTool('frameNavigator', { - mode: 'fit', - }); - }, -}); -// Connector submenu -pie.beginSubmenu({ - label: 'Connector', - icon: ({ rootComponent }) => { - const tool = rootComponent.gfx.tool.currentToolOption$.peek(); - - if (tool.type === 'connector') { - switch (tool.mode) { - case ConnectorMode.Orthogonal: - return ConnectorLWithArrowIcon; - case ConnectorMode.Curve: - return ConnectorCWithArrowIcon; - case ConnectorMode.Straight: - return ConnectorXWithArrowIcon; - } - } - return ConnectorIcon; - }, -}); - -pie.command({ - label: 'Curved', - icon: ConnectorCWithArrowIcon, - action: setEdgelessToolAction(tool => - tool.setTool('connector', { - mode: ConnectorMode.Curve, - }) - ), -}); - -pie.command({ - label: 'Elbowed', - icon: ConnectorXWithArrowIcon, - action: setEdgelessToolAction(tool => - tool.setTool('connector', { - mode: ConnectorMode.Orthogonal, - }) - ), -}); - -pie.command({ - label: 'Straight', - icon: ConnectorLWithArrowIcon, - action: setEdgelessToolAction(tool => - tool.setTool('connector', { - mode: ConnectorMode.Straight, - }) - ), -}); - -pie.colorPicker({ - label: 'Line Color', - active: getActiveConnectorStrokeColor, - onChange: (color: string, { rootComponent }: PieMenuContext) => { - rootComponent.std.get(EditPropsStore).recordLastProps('connector', { - stroke: color as LastProps['connector']['stroke'], - }); - }, - colors: LINE_COLORS.map(color => ({ color })), -}); - -pie.endSubmenu(); - -// Shapes Submenu -pie.beginSubmenu({ - label: 'Shapes', - icon: EdgelessGeneralShapeIcon, -}); - -const shapes = [ - { - type: ShapeType.Rect, - label: 'Rect', - icon: (style: ShapeStyle) => - style === ShapeStyle.General ? SquareIcon : ScribbledSquareIcon, - }, - { - type: ShapeType.Ellipse, - label: 'Ellipse', - icon: (style: ShapeStyle) => - style === ShapeStyle.General ? EllipseIcon : ScribbledEllipseIcon, - }, - { - type: ShapeType.Triangle, - label: 'Triangle', - icon: (style: ShapeStyle) => - style === ShapeStyle.General ? TriangleIcon : ScribbledTriangleIcon, - }, - { - type: ShapeType.Diamond, - label: 'Diamond', - icon: (style: ShapeStyle) => - style === ShapeStyle.General ? DiamondIcon : ScribbledDiamondIcon, - }, -]; - -shapes.forEach(shape => { - pie.command({ - label: shape.label, - icon: ({ rootComponent }) => { - const attributes = - rootComponent.std.get(EditPropsStore).lastProps$.value[ - `shape:${shape.type}` - ]; - return shape.icon(attributes.shapeStyle); - }, - - action: ({ rootComponent }) => { - rootComponent.gfx.tool.setTool('shape', { - shapeName: shape.type, - }); - updateShapeOverlay(rootComponent); - }, - }); -}); - -pie.command({ - label: 'Toggle Style', - icon: ({ rootComponent }) => { - const { shapeStyle } = - rootComponent.std.get(EditPropsStore).lastProps$.value[ - 'shape:roundedRect' - ]; - return shapeStyle === ShapeStyle.General - ? ScribbledStyleIcon - : GeneralStyleIcon; - }, - - action: ({ rootComponent }) => { - const { shapeStyle } = - rootComponent.std.get(EditPropsStore).lastProps$.value[ - 'shape:roundedRect' - ]; - const toggleType = - shapeStyle === ShapeStyle.General - ? ShapeStyle.Scribbled - : ShapeStyle.General; - - rootComponent.std.get(EditPropsStore).recordLastProps('shape:roundedRect', { - shapeStyle: toggleType, - }); - - updateShapeOverlay(rootComponent); - }, -}); - -pie.colorPicker({ - label: 'Fill', - active: getActiveShapeColor('fill'), - onChange: (color: string, { rootComponent }: PieMenuContext) => { - rootComponent.std.get(EditPropsStore).recordLastProps('shape:roundedRect', { - fillColor: color as LastProps['shape:roundedRect']['fillColor'], - }); - updateShapeOverlay(rootComponent); - }, - colors: SHAPE_FILL_COLORS.map(color => ({ color })), -}); - -pie.colorPicker({ - label: 'Stroke', - hollow: true, - active: getActiveShapeColor('stroke'), - onChange: (color: string, { rootComponent }: PieMenuContext) => { - rootComponent.std.get(EditPropsStore).recordLastProps('shape:roundedRect', { - strokeColor: color as LastProps['shape:roundedRect']['strokeColor'], - }); - updateShapeOverlay(rootComponent); - }, - colors: SHAPE_STROKE_COLORS.map(color => ({ color, name: 'Color' })), -}); - -pie.endSubmenu(); - -export const edgelessToolsPieSchema = pie.build(); diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/index.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/index.ts deleted file mode 100644 index cab17eb6d..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/index.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { UIEventStateContext } from '@blocksuite/block-std'; -import { WidgetComponent } from '@blocksuite/block-std'; -import type { IVec } from '@blocksuite/global/utils'; -import { noop } from '@blocksuite/global/utils'; -import { nothing } from 'lit'; -import { state } from 'lit/decorators.js'; - -import { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import type { PieMenuSchema } from './base.js'; -import { PieNodeCenter } from './components/pie-node-center.js'; -import { PieNodeChild } from './components/pie-node-child.js'; -import { PieNodeContent } from './components/pie-node-content.js'; -import { PieCenterRotator } from './components/rotator.js'; -import { edgelessToolsPieSchema } from './config.js'; -import { PieMenu } from './menu.js'; -import { PieManager } from './pie-manager.js'; - -noop(PieNodeContent); -noop(PieNodeCenter); -noop(PieCenterRotator); -noop(PieNodeChild); - -export const AFFINE_PIE_MENU_WIDGET = 'affine-pie-menu-widget'; - -export class AffinePieMenuWidget extends WidgetComponent { - private readonly _handleCursorPos = (ctx: UIEventStateContext) => { - const ev = ctx.get('pointerState'); - const { x, y } = ev.point; - this.mouse = [x, y]; - }; - - private readonly _handleKeyUp = (ctx: UIEventStateContext) => { - if (!this.currentMenu) return; - const ev = ctx.get('keyboardState'); - const { trigger } = this.currentMenu.schema; - - if (trigger({ keyEvent: ev.raw, rootComponent: this.rootComponent })) { - clearTimeout(this.selectOnTrigRelease.timeout); - if (this.selectOnTrigRelease.allow) { - this.currentMenu.selectHovered(); - this.currentMenu.close(); - } - } - }; - - mouse: IVec = [innerWidth / 2, innerHeight / 2]; - - // No action if the currently hovered node is a submenu - selectOnTrigRelease: { allow: boolean; timeout?: NodeJS.Timeout } = { - allow: false, - }; - - get isEnabled() { - return this.doc.awarenessStore.getFlag('enable_pie_menu'); - } - - // if key is released before 100ms then the menu is kept open, else - get isOpen() { - return !!this.currentMenu; - } - - get rootComponent(): EdgelessRootBlockComponent { - const rootComponent = this.block; - if (rootComponent instanceof EdgelessRootBlockComponent) { - return rootComponent; - } - throw new Error('AffinePieMenuWidget is only supported in edgeless'); - } - - private _attachMenu(schema: PieMenuSchema) { - if (this.currentMenu && this.currentMenu.id === schema.id) - return this.currentMenu.close(); - - const [x, y] = this.mouse; - const menu = this._createMenu(schema, { - x, - y, - widgetComponent: this, - }); - this.currentMenu = menu; - - this.selectOnTrigRelease.timeout = setTimeout(() => { - this.selectOnTrigRelease.allow = true; - }, PieManager.settings.SELECT_ON_RELEASE_TIMEOUT); - } - - private _initPie() { - PieManager.setup({ rootComponent: this.rootComponent }); - - this._disposables.add( - PieManager.slots.open.on(this._attachMenu.bind(this)) - ); - } - - private _onMenuClose() { - this.currentMenu = null; - this.selectOnTrigRelease.allow = false; - } - - // on trigger key release it will select the currently hovered menu node - _createMenu( - schema: PieMenuSchema, - { - x, - y, - widgetComponent, - }: { - x: number; - y: number; - widgetComponent: AffinePieMenuWidget; - } - ) { - const menu = new PieMenu(); - menu.id = schema.id; - menu.schema = schema; - menu.position = [x, y]; - menu.rootComponent = widgetComponent.rootComponent; - menu.widgetComponent = widgetComponent; - menu.abortController.signal.addEventListener( - 'abort', - this._onMenuClose.bind(this) - ); - - return menu; - } - - override connectedCallback(): void { - super.connectedCallback(); - - if (!this.isEnabled) return; - - this.handleEvent('keyUp', this._handleKeyUp, { global: true }); - this.handleEvent('pointerMove', this._handleCursorPos, { global: true }); - this.handleEvent( - 'wheel', - ctx => { - const state = ctx.get('defaultState'); - if (state.event instanceof WheelEvent) state.event.stopPropagation(); - }, - { global: true } - ); - - this._initPie(); - } - - override disconnectedCallback(): void { - super.disconnectedCallback(); - PieManager.dispose(); - } - - override render() { - return this.currentMenu ?? nothing; - } - - @state() - accessor currentMenu: PieMenu | null = null; -} - -declare global { - interface HTMLElementTagNameMap { - [AFFINE_PIE_MENU_WIDGET]: AffinePieMenuWidget; - } -} - -PieManager.add(edgelessToolsPieSchema); diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/menu.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/menu.ts deleted file mode 100644 index d914a5330..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/menu.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { CommonUtils } from '@blocksuite/affine-block-surface'; -import type { IVec } from '@blocksuite/global/utils'; -import { - assertEquals, - assertExists, - Slot, - Vec, - WithDisposable, -} from '@blocksuite/global/utils'; -import { html, LitElement, nothing } from 'lit'; -import { property } from 'lit/decorators.js'; -import { styleMap } from 'lit/directives/style-map.js'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import type { PieMenuSchema, PieNodeModel } from './base.js'; -import type { AffinePieMenuWidget } from './index.js'; -import { PieNode } from './node.js'; -import { PieManager } from './pie-manager.js'; -import { pieMenuStyles } from './styles.js'; -import { - getPosition, - isColorNode, - isCommandNode, - isNodeWithAction, - isNodeWithChildren, - isRootNode, - isSubmenuNode, -} from './utils.js'; - -const { toDegree, toRadian } = CommonUtils; - -export class PieMenu extends WithDisposable(LitElement) { - static override styles = pieMenuStyles; - - private readonly _handleKeyDown = (ev: KeyboardEvent) => { - const { key } = ev; - if (key === 'Escape') { - return this.abortController.abort(); - } - - if (ev.code === 'Backspace') { - if (this.selectionChain.length <= 1) return; - const { containerNode } = this.activeNode; - if (containerNode) this.popSelectionChainTo(containerNode); - } - - if (key.match(/\d+/)) { - this.selectChildWithIndex(parseInt(key)); - } - }; - - private readonly _handlePointerMove = (ev: PointerEvent) => { - const { clientX, clientY } = ev; - - const { ACTIVATE_THRESHOLD_MIN } = PieManager.settings; - - const lenSq = this.getActiveNodeToMouseLenSq([clientX, clientY]); - - if (lenSq > ACTIVATE_THRESHOLD_MIN ** 2) { - const [nodeX, nodeY] = this.getActiveNodeRelPos(); - const dx = clientX - nodeX; - const dy = clientY - nodeY; - - const TAU = Math.PI * 2; - const angle = toDegree((Math.atan2(dy, dx) + TAU) % TAU); // convert from [-PI, PI] to [0 TAU] - this.slots.pointerAngleUpdated.emit(angle); - } else { - this.slots.pointerAngleUpdated.emit(null); // acts like a abort signal - } - }; - - private _hoveredNode: PieNode | null = null; - - private _openSubmenuTimeout?: NodeJS.Timeout; - - private readonly selectChildWithIndex = (index: number) => { - const activeNode = this.activeNode; - if (!activeNode || isNaN(index)) return; - - const node = activeNode.querySelector( - `& > affine-pie-node[index='${index}']` - ); - - if (node instanceof PieNode && !isColorNode(node.model)) { - // colors are more than 9 may be another method ? - if (isSubmenuNode(node.model)) this.openSubmenu(node); - else node.select(); - - if (isCommandNode(node.model)) this.close(); - } - }; - - abortController = new AbortController(); - - selectionChain: PieNode[] = []; - - slots = { - pointerAngleUpdated: new Slot(), - requestNodeUpdate: new Slot(), - }; - - get activeNode() { - const node = this.selectionChain[this.selectionChain.length - 1]; - assertExists(node, 'Required atLeast 1 node active'); - return node; - } - - get hoveredNode() { - return this._hoveredNode; - } - - get rootNode() { - const node = this.selectionChain[0]; - assertExists(node, 'No root node'); - return node; - } - - private _createNodeTree(nodeSchema: PieNodeModel): PieNode { - const node = new PieNode(); - const { angle, startAngle, endAngle, label } = nodeSchema; - - node.id = label; - node.model = nodeSchema; - node.angle = angle ?? 0; - node.startAngle = startAngle ?? 0; - node.endAngle = endAngle ?? 0; - node.menu = this; - - if (!isRootNode(nodeSchema)) { - node.slot = 'children-slot'; - const { PIE_RADIUS } = PieManager.settings; - const isColorNode = nodeSchema.type === 'color'; - const radius = isColorNode ? PIE_RADIUS * 0.6 : PIE_RADIUS; - - node.position = getPosition(toRadian(node.angle), [radius, radius]); - } else { - node.position = [0, 0]; - } - - if (isNodeWithChildren(nodeSchema)) { - nodeSchema.children.forEach((childSchema, i) => { - const childNode = this._createNodeTree(childSchema); - childNode.containerNode = node; - childNode.index = i + 1; - childNode.setAttribute('index', childNode.index.toString()); - - node.append(childNode); - }); - } - - return node; - } - - private _setupEvents() { - this._disposables.addFromEvent( - this.widgetComponent, - 'pointermove', - this._handlePointerMove - ); - - this._disposables.addFromEvent(document, 'keydown', this._handleKeyDown); - } - - close() { - this.abortController.abort(); - } - - override connectedCallback(): void { - super.connectedCallback(); - this._setupEvents(); - const root = this._createNodeTree(this.schema.root); - this.selectionChain.push(root); - } - - /** - * Position of the active node relative to the view - */ - getActiveNodeRelPos(): IVec { - const position: IVec = [...this.position]; // use the menus position at start which will be the position of the root node - - for (const node of this.selectionChain) { - position[0] += node.position[0]; - position[1] += node.position[1]; - } - return position; - } - - getActiveNodeToMouseLenSq(mouse: IVec) { - const [x, y] = mouse; - const [nodeX, nodeY] = this.getActiveNodeRelPos(); - - const dx = x - nodeX; - const dy = y - nodeY; - - return Vec.len2([dx, dy]); - } - - getNodeRelPos(node: PieNode): IVec { - const position: IVec = [...this.position]; - let cur: PieNode | null = node; - - while (cur !== null) { - position[0] += cur.position[0]; - position[1] += cur.position[1]; - cur = cur.containerNode; - } - - return position; - } - - isActiveNode(node: PieNode) { - return this.activeNode === node; - } - - isChildOfActiveNode(node: PieNode) { - return node.containerNode === this.activeNode; - } - - openSubmenu(submenu: PieNode) { - assertEquals(submenu.model.type, 'submenu', 'Need node of type submenu'); - - if (isNodeWithAction(submenu.model)) submenu.select(); - - this.selectionChain.push(submenu); - this.setHovered(null); - this.slots.requestNodeUpdate.emit(); - } - - popSelectionChainTo(node: PieNode) { - assertEquals( - isNodeWithChildren(node.model), - true, - 'Required a root node or a submenu node' - ); - - while (this.selectionChain.length > 1 && this.activeNode !== node) { - this.selectionChain.pop(); - } - this.requestUpdate(); - this.slots.requestNodeUpdate.emit(); - } - - override render() { - const [x, y] = this.position; - const menuStyles = { - transform: `translate(${x}px, ${y}px) translate(-50%, -50%)`, - }; - - return html`
    -
    - -
    - ${this.rootNode ?? nothing} -
    -
    `; - } - - selectHovered() { - const { hoveredNode } = this; - - if (hoveredNode) { - hoveredNode.select(); - } - } - - setHovered(node: PieNode | null) { - clearTimeout(this._openSubmenuTimeout); - - this._hoveredNode = node; - - if (!node) return; - - if (isSubmenuNode(node.model)) { - const { openOnHover, timeoutOverride } = node.model; - const { SUBMENU_OPEN_TIMEOUT } = PieManager.settings; - - if (openOnHover !== undefined && !openOnHover) return; - - this._openSubmenuTimeout = setTimeout(() => { - this.openSubmenu(node); - }, timeoutOverride ?? SUBMENU_OPEN_TIMEOUT); - } - } - - @property({ attribute: false }) - accessor position!: IVec; - - @property({ attribute: false }) - accessor rootComponent!: EdgelessRootBlockComponent; - - @property({ attribute: false }) - accessor schema!: PieMenuSchema; - - @property({ attribute: false }) - accessor widgetComponent!: AffinePieMenuWidget; -} diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/node.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/node.ts deleted file mode 100644 index 69cd37e4a..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/node.ts +++ /dev/null @@ -1,182 +0,0 @@ -import type { IVec } from '@blocksuite/global/utils'; -import { WithDisposable } from '@blocksuite/global/utils'; -import { html, LitElement } from 'lit'; -import { property, state } from 'lit/decorators.js'; - -import type { PieNodeModel } from './base.js'; -import type { PieMenu } from './menu.js'; -import { pieNodeStyles } from './styles.js'; -import { - isAngleBetween, - isColorNode, - isCommandNode, - isNodeWithAction, - isNodeWithChildren, - isRootNode, -} from './utils.js'; - -export class PieNode extends WithDisposable(LitElement) { - static override styles = pieNodeStyles; - - private readonly _handleChildNodeClick = () => { - this.select(); - if (isCommandNode(this.model)) this.menu.close(); - }; - - private readonly _handleGoBack = () => { - // If the node is not active and if it is hovered then we can go back to that node - if (this.menu.activeNode !== this) { - this.menu.popSelectionChainTo(this); - } - }; - - private readonly _onPointerAngleUpdated = (angle: number | null) => { - this._rotatorAngle = angle; - this.menu.activeNode.requestUpdate(); - - if (isRootNode(this.model) || !this.menu.isChildOfActiveNode(this)) return; - if (angle === null) { - this._isHovering = false; - this.menu.setHovered(null); - return; - } - - if (isAngleBetween(angle, this.startAngle, this.endAngle)) { - if (this.menu.hoveredNode !== this) { - this._isHovering = true; - this.menu.setHovered(this); - } - } else { - this._isHovering = false; - } - }; - - private _rotatorAngle: number | null = null; - - get icon() { - const icon = this.model.icon; - if (typeof icon === 'function') { - const { menu } = this; - const { rootComponent, widgetComponent } = menu; - return icon({ - rootComponent, - menu, - widgetComponent, - node: this, - }); - } - return icon; - } - - private _renderCenterNode() { - const isActiveNode = this.isActive(); - - return html` - - - - `; - } - - private _renderChildNode() { - const visible = this.menu.isChildOfActiveNode(this); - return html` - `; - } // for selecting with keyboard - - private _setupEvents() { - this._disposables.add( - this.menu.slots.pointerAngleUpdated.on(this._onPointerAngleUpdated) - ); - - this._disposables.add( - this.menu.slots.requestNodeUpdate.on(() => this.requestUpdate()) - ); - } - - override connectedCallback(): void { - super.connectedCallback(); - this._setupEvents(); - } - - isActive() { - return this.menu.isActiveNode(this); - } - - isCenterNode() { - return ( - isNodeWithChildren(this.model) && this.menu.selectionChain.includes(this) - ); - } - - protected override render() { - return this.isCenterNode() - ? this._renderCenterNode() - : this._renderChildNode(); - } - - select() { - const schema = this.model; - - if (isRootNode(schema)) return; - - const ctx = { - rootComponent: this.menu.rootComponent, - menu: this.menu, - widgetComponent: this.menu.widgetComponent, - node: this, - }; - - if (isNodeWithAction(schema)) { - schema.action(ctx); - } else if (isColorNode(schema)) { - schema.onChange(schema.color, ctx); - } - - this.requestUpdate(); - } - - @state() - private accessor _isHovering = false; - - @property({ attribute: false }) - accessor angle!: number; - - @property({ attribute: false }) - accessor containerNode: PieNode | null = null; - - @property({ attribute: false }) - accessor endAngle!: number; - - @property({ attribute: false }) - accessor index!: number; - - @property({ attribute: false }) - accessor menu!: PieMenu; - - @property({ attribute: false }) - accessor model!: PieNodeModel; - - @property({ attribute: false }) - accessor position!: IVec; - - @property({ attribute: false }) - accessor startAngle!: number; -} - -declare global { - interface HTMLElementTagNameMap { - 'pie-node': PieNode; - } -} diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/pie-builder.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/pie-builder.ts deleted file mode 100644 index 8d4246ea7..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/pie-builder.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { assertExists } from '@blocksuite/global/utils'; - -import { ColorUnit } from '../../edgeless/components/panel/color-panel.js'; -import type { - ActionFunction, - PieColorNodeModel, - PieCommandNodeModel, - PieMenuContext, - PieMenuSchema, - PieNodeModel, - PieSubmenuNodeModel, -} from './base.js'; -import { PieManager } from './pie-manager.js'; -import { calcNodeAngles, calcNodeWedges, isNodeWithChildren } from './utils.js'; - -export interface IPieColorPickerNodeProps { - label: string; - active: (ctx: PieMenuContext) => string; - onChange: PieColorNodeModel['onChange']; - openOnHover?: PieSubmenuNodeModel['openOnHover']; - hollow?: boolean; - colors: { color: string }[]; -} - -type PieBuilderConstructorProps = Omit< - PieMenuSchema, - 'root' | 'angle' | 'startAngle' | 'endAngle' | 'disabled' -> & { icon: PieNodeModel['icon'] }; - -export class PieMenuBuilder { - private _schema: PieMenuSchema | null = null; - - private _stack: PieNodeModel[] = []; - - constructor(base: PieBuilderConstructorProps) { - this._schema = { - ...base, - root: { - type: 'root', - children: [], - label: base.label, - icon: base.icon, - disabled: false, - }, - }; - this._stack.push(this._schema.root); - } - - private _computeAngles(node: PieNodeModel) { - if ( - !isNodeWithChildren(node) || - !node.children || - node.children.length === 0 - ) { - return; - } - const parentAngle = - node.angle == undefined ? undefined : (node.angle + 180) % 360; - const angles = calcNodeAngles(node.children, parentAngle); - const wedges = calcNodeWedges(angles, parentAngle); - - for (let i = 0; i < node.children.length; ++i) { - const child = node.children[i]; - child.angle = angles[i]; - child.startAngle = wedges[i].start; - child.endAngle = wedges[i].end; - - this._computeAngles(child); - } - } - - private _currentNode(): PieNodeModel { - const node = this._stack[this._stack.length - 1]; - assertExists(node, 'No node active'); - return node; - } - - beginSubmenu( - node: Omit, - action?: PieSubmenuNodeModel['action'] - ) { - const curNode = this._currentNode(); - const submenuNode: PieSubmenuNodeModel = { - openOnHover: true, - ...node, - type: 'submenu', - role: action ? 'default' : 'command', - action, - children: [], - }; - if (submenuNode.action !== undefined) - submenuNode.timeoutOverride = - PieManager.settings.EXPANDABLE_ACTION_NODE_TIMEOUT; - - if (isNodeWithChildren(curNode)) { - curNode.children.push(submenuNode); - } - - this._stack.push(submenuNode); - - return this; - } - - build() { - const schema = this._schema; - assertExists(schema); - this._computeAngles(schema.root); - - this._schema = null; - this._stack = []; - return schema; - } - - colorPicker(props: IPieColorPickerNodeProps) { - const hollow = props.hollow ?? false; - - const icon = (ctx: PieMenuContext) => { - const color = props.active(ctx); - - return ColorUnit(color, { hollowCircle: hollow }); - }; - - const colorPickerNode: PieSubmenuNodeModel = { - type: 'submenu', - icon, - label: props.label, - role: 'color-picker', - openOnHover: props.openOnHover ?? true, - children: props.colors.map(({ color }) => ({ - icon: () => ColorUnit(color, { hollowCircle: hollow }), - type: 'color', - hollowCircle: hollow, - label: color, - color: color, - onChange: props.onChange, - })), - }; - - const curNode = this._currentNode(); - if (isNodeWithChildren(curNode)) { - curNode.children.push(colorPickerNode); - } - } - - command(node: Omit) { - const curNode = this._currentNode(); - const actionNode: PieCommandNodeModel = { ...node, type: 'command' }; - - if (isNodeWithChildren(curNode)) { - curNode.children.push(actionNode); - } - - return this; - } - - endSubmenu() { - if (this._stack.length === 1) - throw new Error('Cant end submenu already on the root node'); - this._stack.pop(); - - return this; - } - - expandableCommand( - node: Omit & { - action: ActionFunction; - submenus: (pie: PieMenuBuilder) => void; - } - ) { - const { icon, label } = node; - this.beginSubmenu({ icon, label }, node.action); - node.submenus(this); - this.endSubmenu(); - } - - reset(base: PieBuilderConstructorProps) { - this._stack = []; - this._schema = { - ...base, - root: { type: 'root', children: [], label: base.label }, - }; - - this._stack.push(this._schema.root); - } -} diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/pie-manager.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/pie-manager.ts deleted file mode 100644 index 31e4c203e..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/pie-manager.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { assertExists } from '@blocksuite/global/utils'; -import { Slot } from '@blocksuite/store'; - -import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import type { PieMenuId } from '../../types.js'; -import type { PieMenuSchema } from './base.js'; - -/** - * Static class for managing pie menus - */ - -export class PieManager { - private static registeredSchemas: Record = {}; - - private static readonly schemas = new Set(); - - static settings = { - /** - * Specifies the distance between the root-node and the child-nodes - */ - PIE_RADIUS: 150, - /** - * After the specified time if trigger is released the menu will select the currently hovered node\ - * If released before the time the pie menu will stay open and you can select with mouse or the trigger key\ - * Time is in `milliseconds` - * @default 150 - */ - SELECT_ON_RELEASE_TIMEOUT: 150, - - /** - * Distance from the center of the active node to start focusing a child node - */ - ACTIVATE_THRESHOLD_MIN: 60, - - /** - * Time delay to open submenu after hovering a submenu node - */ - SUBMENU_OPEN_TIMEOUT: 200, - - EXPANDABLE_ACTION_NODE_TIMEOUT: 300, - }; - - static slots = { - open: new Slot(), - }; - - private static _getSchema(id: string) { - const schema = this.registeredSchemas[id]; - assertExists(schema); - return schema; - } - - private static _register(schema: PieMenuSchema) { - const { id } = schema; - - if (this.registeredSchemas[id]) { - return; - } - - this.registeredSchemas[id] = schema; - } - - private static _setupTriggers(rootComponent: EdgelessRootBlockComponent) { - Object.values(this.registeredSchemas).forEach(schema => { - const { trigger } = schema; - - rootComponent.handleEvent( - 'keyDown', - ctx => { - const ev = ctx.get('keyboardState'); - - if (trigger({ keyEvent: ev.raw, rootComponent }) && !ev.raw.repeat) { - this.open(schema.id); - } - }, - { global: true } - ); - }); - } - - static add(schema: PieMenuSchema) { - return this.schemas.add(schema); - } - - static dispose() { - this.registeredSchemas = {}; - } - - static open(id: PieMenuId) { - this.slots.open.emit(this._getSchema(id)); - } - - static remove(schema: PieMenuSchema) { - return this.schemas.delete(schema); - } - - static setup({ - rootComponent, - }: { - rootComponent: EdgelessRootBlockComponent; - }) { - this.schemas.forEach(schema => this._register(schema)); - this._setupTriggers(rootComponent); - } -} diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/styles.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/styles.ts deleted file mode 100644 index 5930beca1..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/styles.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { css } from 'lit'; - -export const pieMenuStyles = css` - .menu-container { - user-select: none; - z-index: var(--affine-z-index-popover); - isolation: isolate; - } - - .pie-menu-container > .overlay { - top: 0; - left: 0; - height: 100vh; - width: 100vw; - position: fixed; - z-index: var(--affine-z-index-popover); - } - - .pie-menu { - position: fixed; - top: 0; - left: 0; - box-sizing: border-box; - z-index: calc( - var(--affine-z-index-popover) + 10 - ); /* This is important or else will hover will not work */ - } -`; - -export const pieNodeStyles = css` - .pie-node { - position: absolute; - background: var(--affine-background-overlay-panel-color); - user-select: none; - box-shadow: var(--affine-shadow-2); - border: 2px solid var(--affine-border-color); - border-radius: 50%; - display: flex; - font-size: 0.8rem; - align-items: center; - justify-content: center; - text-align: center; - transition: all 250ms cubic-bezier(0.775, 1.325, 0.535, 1); - } - - @keyframes my-anim { - 0% { - transform: translate(0, 0); - opacity: 0; - } - 40% { - opacity: 0; - } - 100% { - opacity: 100; - } - } -`; diff --git a/blocksuite/blocks/src/root-block/widgets/pie-menu/utils.ts b/blocksuite/blocks/src/root-block/widgets/pie-menu/utils.ts deleted file mode 100644 index 48bd84ebd..000000000 --- a/blocksuite/blocks/src/root-block/widgets/pie-menu/utils.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { EditPropsStore } from '@blocksuite/affine-shared/services'; -import type { ToolController } from '@blocksuite/block-std/gfx'; -import type { IVec } from '@blocksuite/global/utils'; - -import { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; -import { ShapeTool } from '../../edgeless/gfx-tool/shape-tool.js'; -import type { - ActionFunction, - IPieNodeWithAction, - PieColorNodeModel, - PieCommandNodeModel, - PieMenuContext, - PieNodeModel, - PieNonRootNode, - PieRootNodeModel, - PieSubmenuNodeModel, -} from './base.js'; - -export function updateShapeOverlay(rootComponent: EdgelessRootBlockComponent) { - const controller = rootComponent.gfx.tool.currentTool$.peek(); - if (controller instanceof ShapeTool) { - controller.createOverlay(); - } -} - -export function getActiveShapeColor(type: 'fill' | 'stroke') { - return ({ rootComponent }: PieMenuContext) => { - if (rootComponent instanceof EdgelessRootBlockComponent) { - const props = - rootComponent.std.get(EditPropsStore).lastProps$.value[ - 'shape:roundedRect' - ]; - const color = type == 'fill' ? props.fillColor : props.strokeColor; - return color.toString(); - } - return ''; - }; -} - -export function getActiveConnectorStrokeColor({ - rootComponent, -}: PieMenuContext) { - if (rootComponent instanceof EdgelessRootBlockComponent) { - const props = - rootComponent.std.get(EditPropsStore).lastProps$.value.connector; - const color = props.stroke; - return color.toString(); - } - return ''; -} - -export function setEdgelessToolAction( - callback: (tool: ToolController) => void -): ActionFunction { - return ({ rootComponent }) => { - callback(rootComponent.gfx.tool); - }; -} - -export function getPosition(angleRad: number, v: IVec): IVec { - const x = Math.cos(angleRad) * v[0]; - const y = Math.sin(angleRad) * v[1]; - return [x, y]; -} - -export function isNodeWithChildren( - node: PieNodeModel -): node is PieNodeModel & { children: PieNonRootNode[] } { - return 'children' in node; -} - -export function isRootNode(model: PieNodeModel): model is PieRootNodeModel { - return model.type === 'root'; -} - -export function isSubmenuNode( - model: PieNodeModel -): model is PieSubmenuNodeModel { - return model.type === 'submenu'; -} - -export function isCommandNode( - model: PieNodeModel -): model is PieCommandNodeModel { - return model.type === 'command'; -} - -export function isColorNode(model: PieNodeModel): model is PieColorNodeModel { - return model.type === 'color'; -} - -export function isNodeWithAction( - node: PieNodeModel -): node is IPieNodeWithAction { - return 'action' in node && typeof node.action === 'function'; -} - -//------------------------------------------------------------------------------------ -// credits: https://github.com/kando-menu/kando/blob/main/src/renderer/math/index.ts -//------------------------------------------------------------------------------------ -export function calcNodeAngles( - node: { angle?: number }[], - parentAngle?: number -): number[] { - const nodeAngles: number[] = []; - - // Shouldn't happen, but who knows... - if (node.length == 0) { - return nodeAngles; - } - - // We begin by storing all fixed angles. - const fixedAngles: { angle: number; index: number }[] = []; - node.forEach((item, index) => { - if (item.angle && item.angle >= 0) { - fixedAngles.push({ angle: item.angle, index: index }); - } - }); - - // Make sure that the parent link does not collide with a fixed item. For now, we - // just move the fixed angle a tiny bit. This is somewhat error-prone as it may - // collide with another fixed angle now. Maybe this could be solved in a better way? - // Maybe some global minimum angular spacing of items? - if (parentAngle != undefined) { - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < fixedAngles.length; i++) { - if (Math.abs(fixedAngles[i].angle - parentAngle) < 0.0001) { - fixedAngles[i].angle += 0.1; - } - } - } - - // Make sure that the fixed angles are between 0° and 360°. - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < fixedAngles.length; i++) { - fixedAngles[i].angle = fixedAngles[i].angle % 360; - } - - // Make sure that the fixed angles increase monotonically. If a fixed angle is larger - // than the next one, the next one will be ignored. - for (let i = 0; i < fixedAngles.length - 1; ) { - if (fixedAngles[i].angle > fixedAngles[i + 1].angle) { - fixedAngles.splice(i + 1, 1); - } else { - ++i; - } - } - - // If no item has a fixed angle, we assign one to the first item. If there is no - // parent item, this is on the top (0°). Else, the angular space will be evenly - // distributed to all child items and the first item will be at the first possible - // location with an angle > 0. - if (fixedAngles.length == 0) { - let firstAngle = 0; - if (parentAngle != undefined) { - const wedgeSize = 360 / (node.length + 1); - let minAngle = 360; - for (let i = 0; i < node.length; i++) { - minAngle = Math.min( - minAngle, - (parentAngle + (i + 1) * wedgeSize) % 360 - ); - } - firstAngle = minAngle; - } - fixedAngles.push({ angle: firstAngle, index: 0 }); - nodeAngles[0] = firstAngle; - } - - // Now we iterate through the fixed angles, always considering wedges between - // consecutive pairs of fixed angles. If there is only one fixed angle, there is also - // only one 360°-wedge. - for (let i = 0; i < fixedAngles.length; i++) { - const wedgeBeginIndex = fixedAngles[i].index; - const wedgeBeginAngle = fixedAngles[i].angle; - const wedgeEndIndex = fixedAngles[(i + 1) % fixedAngles.length].index; - let wedgeEndAngle = fixedAngles[(i + 1) % fixedAngles.length].angle; - - // The fixed angle can be stored in our output. - nodeAngles[wedgeBeginIndex] = wedgeBeginAngle; - - // Make sure we loop around. - if (wedgeEndAngle <= wedgeBeginAngle) { - wedgeEndAngle += 360; - } - - // Calculate the number of items between the begin and end indices. - let wedgeItemCount = - (wedgeEndIndex - wedgeBeginIndex - 1 + node.length) % node.length; - - // We have one item more if the parent link is inside our wedge. - let parentInWedge = false; - - if (parentAngle != undefined) { - // It can be that the parent link is inside the current wedge, but it's angle is - // one full turn off. - if (parentAngle < wedgeBeginAngle) { - parentAngle += 360; - } - - parentInWedge = - parentAngle > wedgeBeginAngle && parentAngle < wedgeEndAngle; - if (parentInWedge) { - wedgeItemCount += 1; - } - } - - // Calculate the angular difference between consecutive items in the current wedge. - const wedgeItemGap = - (wedgeEndAngle - wedgeBeginAngle) / (wedgeItemCount + 1); - - // Now we assign an angle to each item between the begin and end indices. - let index = (wedgeBeginIndex + 1) % node.length; - let count = 1; - let parentGapRequired = parentInWedge; - - while (index != wedgeEndIndex) { - let itemAngle = wedgeBeginAngle + wedgeItemGap * count; - - // Insert gap for parent link if required. for connector - if ( - parentGapRequired && - itemAngle + wedgeItemGap / 2 - (parentAngle ?? 0) > 0 - ) { - count += 1; - itemAngle = wedgeBeginAngle + wedgeItemGap * count; - parentGapRequired = false; - } - - nodeAngles[index] = itemAngle % 360; - - index = (index + 1) % node.length; - count += 1; - } - } - - return nodeAngles; -} - -export function calcNodeWedges( - nodeAngles: number[], - parentAngle?: number -): { start: number; end: number }[] { - // This should never happen, but who knows... - if (nodeAngles.length === 0 && parentAngle === undefined) { - return []; - } - - // If the node has a single child but no parent (e.g. it's the root node), we can - // simply return a full circle. - if (nodeAngles.length === 1 && parentAngle === undefined) { - return [{ start: 0, end: 360 }]; - } - - // If the node has a single child and a parent, we can set the start and end - // angles to the center angles. - if (nodeAngles.length === 1 && parentAngle !== undefined) { - let start = parentAngle; - let center = nodeAngles[0]; - let end = parentAngle + 360; - - [start, center, end] = normalizeConsecutiveAngles(start, center, end); - [start, end] = scaleWedge(start, center, end, 0.5); - - return [{ start: start, end: end }]; - } - - // In all other cases, we loop through the items and compute the wedges. If the parent - // angle happens to be inside a wedge, we crop the wedge accordingly. - const wedges: { start: number; end: number }[] = []; - - for (let i = 0; i < nodeAngles.length; i++) { - let start = nodeAngles[(i + nodeAngles.length - 1) % nodeAngles.length]; - let center = nodeAngles[i]; - let end = nodeAngles[(i + 1) % nodeAngles.length]; - - [start, center, end] = normalizeConsecutiveAngles(start, center, end); - - if (parentAngle !== undefined) { - [start, end] = cropWedge(start, center, end, parentAngle); - [start, center, end] = normalizeConsecutiveAngles(start, center, end); - } - - [start, end] = scaleWedge(start, center, end, 0.5); - - wedges.push({ start: start, end: end }); - } - - return wedges; -} -export function isAngleBetween( - angle: number, - start: number, - end: number -): boolean { - return ( - (angle > start && angle <= end) || - (angle - 360 > start && angle - 360 <= end) || - (angle + 360 > start && angle + 360 <= end) - ); -} - -function normalizeConsecutiveAngles( - start: number, - center: number, - end: number -) { - while (center < start) { - center += 360; - } - - while (end < center) { - end += 360; - } - - while (center >= 360) { - start -= 360; - center -= 360; - end -= 360; - } - - return [start, center, end]; -} - -function cropWedge( - start: number, - center: number, - end: number, - cropAngle: number -) { - if (isAngleBetween(cropAngle, start, center)) { - start = cropAngle; - } - - if (isAngleBetween(cropAngle, center, end)) { - end = cropAngle; - } - - return [start, end]; -} -function scaleWedge(start: number, center: number, end: number, scale: number) { - start = center - (center - start) * scale; - end = center + (end - center) * scale; - - return [start, end]; -}