feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inject, injectable } from 'inversify';
import { DebugBarWidget } from '../widget/react-widgets/debug-bar-widget';
import { createPortal } from '../utils';
import { ApplicationShell } from '../shell/application-shell';
import { ViewOptions } from '../constants/view-options';
// 控制 debug
@injectable()
export class DebugService {
@inject(ViewOptions) viewOptions: ViewOptions;
@inject(ApplicationShell) shell: ApplicationShell;
@inject(DebugBarWidget) debugBarWidget: DebugBarWidget;
show() {
this.debugBarWidget.show();
this.debugBarWidget.update();
}
hide() {
this.debugBarWidget.hide();
this.debugBarWidget.update();
}
createPortal() {
const originRenderer = this.debugBarWidget.render.bind(this.debugBarWidget);
const portal = createPortal(
this.debugBarWidget,
originRenderer,
this.viewOptions.widgetFallbackRender!,
);
this.shell.node.insertBefore(this.debugBarWidget.node, null);
this.hide();
return portal;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inject, injectable } from 'inversify';
import { type URI } from '@coze-project-ide/core';
import { WidgetManager } from '../widget-manager';
import { type ReactWidget } from '../widget/react-widget';
import { ViewRenderer } from '../view-renderer';
import { ApplicationShell } from '../shell';
import { Drag } from '../lumino/dragdrop';
import { MimeData } from '../lumino/coreutils';
export interface DragPropsType {
/**
* 拖拽打开分屏的 URI
*/
uris: URI[];
/**
* startDrag event 位置数据
*/
position: {
clientX: number;
clientY: number;
};
/**
* 拖拽元素回显,不传不展示
*/
dragImage?: HTMLElement;
/**
* 拖拽完成后回调
* action: 'move' | 'copy' | 'link' | 'none'
*/
callback: (action: Drag.DropAction) => void;
backdropTransform?: Drag.BackDropTransform;
}
/**
* DragService 主要用于分屏操作
*/
@injectable()
export class DragService {
@inject(ApplicationShell) shell: ApplicationShell;
@inject(WidgetManager) widgetManager: WidgetManager;
@inject(ViewRenderer) viewRenderer: ViewRenderer;
/**
* 业务侧手动拖拽触发分屏(侧边栏文件树拖拽进入开始分屏)
*/
startDrag({
uris,
position,
dragImage,
callback,
backdropTransform,
}: DragPropsType) {
const { clientX, clientY } = position;
const mimeData = new MimeData();
const factory = async () => {
const widgets: ReactWidget[] = [];
await Promise.all(
uris.map(async uri => {
const factory = this.widgetManager.getFactoryFromURI(uri)!;
const widget = await this.widgetManager.getOrCreateWidgetFromURI(
uri,
factory,
);
this.viewRenderer.addReactPortal(widget);
widgets.push(widget);
}),
);
return widgets;
};
mimeData.setData('application/vnd.lumino.widget-factory', factory);
const drag = new Drag({
document,
mimeData,
dragImage,
proposedAction: 'move',
supportedActions: 'move',
/**
* 仅支持在主面板区域分屏
*/
source: this.shell.mainPanel,
backdropTransform,
});
drag.start(clientX, clientY).then(callback);
}
}

View File

@@ -0,0 +1,315 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type React from 'react';
import { type Root, createRoot } from 'react-dom/client';
import { inject, injectable } from 'inversify';
import {
type Disposable,
DisposableCollection,
} from '@flowgram-adapter/common';
import { LabelService } from '@coze-project-ide/core';
import { HOVER_TOOLTIP_LABEL } from '../constants';
export function createDisposableTimer(
...args: Parameters<typeof setTimeout>
): Disposable {
const handle = setTimeout(...args);
return { dispose: () => clearTimeout(handle) };
}
export function animationFrame(n = 1): Promise<void> {
return new Promise(resolve => {
function frameFunc(): void {
if (n <= 0) {
resolve();
} else {
n--;
requestAnimationFrame(frameFunc);
}
}
frameFunc();
});
}
export type HoverPosition = 'left' | 'right' | 'top' | 'bottom';
export namespace HoverPosition {
export function invertIfNecessary(
position: HoverPosition,
target: DOMRect,
host: DOMRect,
totalWidth: number,
totalHeight: number,
enableCustomHost: boolean,
): HoverPosition {
if (position === 'left') {
if (enableCustomHost) {
if (target.left - target.width < 0) {
return 'right';
}
} else if (target.left - host.width < 0) {
return 'right';
}
} else if (position === 'right') {
if (enableCustomHost) {
if (target.right + target.width > totalWidth) {
return 'left';
}
} else if (target.right + host.width > totalWidth) {
return 'left';
}
} else if (position === 'top') {
if (enableCustomHost) {
if (target.top - target.height < 0) {
return 'bottom';
}
} else if (target.top - host.height < 0) {
return 'bottom';
}
} else if (position === 'bottom') {
if (enableCustomHost) {
if (target.bottom + target.height > totalHeight) {
return 'top';
}
} else if (target.bottom + host.height > totalHeight) {
return 'top';
}
}
return position;
}
}
export interface HoverRequest {
content: string | HTMLElement | React.ReactNode;
target: HTMLElement;
position: HoverPosition;
cssClasses?: string[];
visualPreview?: (width: number) => HTMLElement | undefined;
/** hover 位置偏移 */
offset?: number;
}
@injectable()
export class HoverService {
@inject(LabelService) labelService: LabelService;
protected static hostClassName = 'flow-hover';
protected static styleSheetId = 'flow-hover-style';
protected _hoverHost: HTMLElement | undefined;
reactRoot: Root | null = null;
protected get hoverHost(): HTMLElement {
if (!this._hoverHost) {
this._hoverHost = document.createElement('div');
this._hoverHost.classList.add(HoverService.hostClassName);
this._hoverHost.style.position = 'absolute';
}
return this._hoverHost;
}
protected pendingTimeout: Disposable | undefined;
protected hoverTarget: HTMLElement | undefined;
protected lastHidHover = Date.now();
protected enableCustomHost = false;
// protected timer: any = null;
protected readonly disposeOnHide = new DisposableCollection();
enableCustomHoverHost() {
if (!this._hoverHost) {
this.enableCustomHost = true;
this._hoverHost = document.createElement('div');
this.reactRoot = createRoot(this._hoverHost);
this._hoverHost.style.position = 'absolute';
}
}
requestHover(r: HoverRequest): void {
if (r.target !== this.hoverTarget) {
this.cancelHover();
// clearTimeout(this.timer);
this.pendingTimeout = createDisposableTimer(
() => this.renderHover(r),
this.getHoverDelay(),
);
}
}
protected async renderHover(request: HoverRequest): Promise<void> {
const host = this.hoverHost;
let firstChild: HTMLElement | undefined;
const { target, content, position, cssClasses, offset } = request;
if (cssClasses) {
host.classList.add(...cssClasses);
}
this.hoverTarget = target;
if (!this.reactRoot && content instanceof HTMLElement) {
host.appendChild(content);
firstChild = content;
} else if (!this.reactRoot && typeof content === 'string') {
host.textContent = content;
}
host.style.top = '0px';
host.style.left = '0px';
document.body.append(host);
if (request.visualPreview) {
const width = firstChild
? firstChild.offsetWidth
: this.hoverHost.offsetWidth;
const visualPreview = request.visualPreview(width);
if (visualPreview) {
host.appendChild(visualPreview);
}
}
await animationFrame();
const newPos = this.setHostPosition(target, host, position, offset);
if (this.reactRoot) {
const renderer = this.labelService.renderer(HOVER_TOOLTIP_LABEL, {
content,
position: newPos,
key: new Date().getTime(),
});
this.reactRoot.render(renderer);
}
this.disposeOnHide.push({
dispose: () => {
this.lastHidHover = Date.now();
host.classList.remove(newPos);
if (cssClasses) {
host.classList.remove(...cssClasses);
}
},
});
this.listenForMouseOut();
}
protected listenForMouseOut(): void {
const handleMouseMove = (e: MouseEvent) => {
if (!this.hoverTarget) {
return;
}
if (
e.target instanceof Node &&
!this.hoverHost.contains(e.target) &&
!this.hoverTarget?.contains(e.target)
) {
// clearTimeout(this.timer);
// this.timer = setTimeout(() => {
// this.cancelHover();
// }, 300);
this.cancelHover();
}
};
document.addEventListener('mousemove', handleMouseMove);
this.disposeOnHide.push({
dispose: () => document.removeEventListener('mousemove', handleMouseMove),
});
}
protected getHoverDelay(): number {
return Date.now() - this.lastHidHover < 200 ? 0 : 200;
}
protected setHostPosition(
target: HTMLElement,
host: HTMLElement,
position: HoverPosition,
offset?: number,
): HoverPosition {
const hostRect = host.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
const documentHeight = document.documentElement.scrollHeight;
const documentWidth = document.body.getBoundingClientRect().width;
const calcOffset = offset || 0;
position = HoverPosition.invertIfNecessary(
position,
targetRect,
hostRect,
documentWidth,
documentHeight,
this.enableCustomHost,
);
if (position === 'top' || position === 'bottom') {
const targetMiddleWidth = targetRect.left + targetRect.width / 2;
const middleAlignment = targetMiddleWidth - hostRect.width / 2;
const furthestRight = Math.min(
documentWidth - hostRect.width,
middleAlignment,
);
const top =
position === 'top'
? targetRect.top - hostRect.height + calcOffset
: targetRect.bottom - calcOffset;
const left = Math.max(0, furthestRight);
host.style.top = `${top}px`;
host.style.left = `${left}px`;
} else {
const targetMiddleHeight = targetRect.top + targetRect.height / 2;
const middleAlignment = targetMiddleHeight - hostRect.height / 2;
const furthestTop = Math.min(
documentHeight - hostRect.height,
middleAlignment,
);
const left =
position === 'left'
? targetRect.left - hostRect.width - calcOffset
: targetRect.right + calcOffset;
const top = Math.max(0, furthestTop);
host.style.left = `${left}px`;
host.style.top = `${top}px`;
}
host.classList.add(position);
return position;
}
protected unRender(): void {
this.hoverHost.remove();
this.hoverHost.replaceChildren();
}
cancelHover(): void {
if (this.reactRoot) {
const renderer = this.labelService.renderer(HOVER_TOOLTIP_LABEL, {
visible: false,
key: new Date().getTime(),
});
this.reactRoot.render(renderer);
} else {
this.unRender();
}
this.pendingTimeout?.dispose();
this.disposeOnHide.dispose();
this.hoverTarget = undefined;
}
}

View File

@@ -0,0 +1,222 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inject, injectable } from 'inversify';
import { Emitter } from '@flowgram-adapter/common';
import { type URI } from '@coze-project-ide/core';
import { WidgetOpenHandler } from '../widget/widget-open-handler';
import { type ReactWidget } from '../widget/react-widget';
import { type FlowDockPanel } from '../widget/dock-panel';
import { type CustomTitleType, LayoutPanelType } from '../types';
import { ApplicationShell } from '../shell';
import { type DockLayout } from '../lumino/widgets';
import { ALL_PANEL_TYPES } from '../constants/view';
@injectable()
export class ViewService {
@inject(ApplicationShell) shell: ApplicationShell;
@inject(WidgetOpenHandler) protected readonly openHandler: WidgetOpenHandler;
private isFullScreenMode = false;
private onFullScreenModeChangeEmitter = new Emitter<boolean>();
onFullScreenModeChange = this.onFullScreenModeChangeEmitter.event;
private prevPanelMap = new Map<LayoutPanelType, boolean>();
/**
* 唤起底部面板
*/
toggleBottomLayout() {
this.shell.bottomSplitLayout.setRelativeSizes([0.7, 0.3]);
}
/**
* 隐藏底部面板
*/
hideBottomLayout() {
this.shell.bottomSplitLayout.setRelativeSizes([1, 0]);
}
/**
* 获取所有打开的 tab title
*/
getOpenTitles() {
let titles: CustomTitleType[] = [];
const tabBars = (
this.shell.mainPanel.layout as unknown as DockLayout
).tabBars();
for (const tabBar of tabBars) {
titles = titles.concat(tabBar.titles as CustomTitleType[]);
}
return titles;
}
/**
* 获取当前 panel 打开的所有 tab
*/
getAllTabsFromArea(
area: LayoutPanelType.MAIN_PANEL | LayoutPanelType.BOTTOM_PANEL,
) {
const widgets =
area === LayoutPanelType.MAIN_PANEL
? this.shell.mainPanel.widgets()
: this.shell.bottomPanel.widgets();
const dockPanels: any[] = [];
for (const dockPanel of widgets) {
dockPanels.push(dockPanel);
}
return dockPanels;
}
/**
* 关闭除了当前 tab 以外的所有 tab
*/
closeOtherTabs(dispose = true) {
try {
const parentWidget = this.shell.currentWidget?.parent;
if (!parentWidget) {
return;
}
const widgets = (parentWidget as FlowDockPanel).tabBars();
for (const customTabBar of widgets) {
[...customTabBar.titles].map(title => {
if (title.label !== customTabBar.currentTitle?.label) {
customTabBar.removeTab(title);
if (dispose) {
title.owner.dispose();
}
}
});
}
} catch (e) {
console.error(e);
}
}
/**
* 打开主面板的下一个 tab
*/
openNextTab() {
const tabBars = (
this.shell.mainPanel.layout as unknown as DockLayout
).tabBars();
for (const tabbar of tabBars) {
const idx = tabbar.titles.findIndex(
title => title.owner === this.shell.currentWidget,
);
if (idx !== -1) {
const nextUri = (
tabbar.titles[(idx + 1) % tabbar.titles.length].owner as ReactWidget
)?.getResourceURI();
if (nextUri) {
this.openHandler.open(nextUri);
}
}
}
}
/**
* 打开主面板的上一个 tab
*/
openLastTab() {
const tabBars = (
this.shell.mainPanel.layout as unknown as DockLayout
).tabBars();
for (const tabbar of tabBars) {
const idx = tabbar.titles.findIndex(
title => title.owner === this.shell.currentWidget,
);
if (idx !== -1) {
const nextUri = (
tabbar.titles[(idx - 1 + tabbar.titles.length) % tabbar.titles.length]
.owner as ReactWidget
)?.getResourceURI();
if (nextUri) {
this.openHandler.open(nextUri);
}
}
}
}
/**
* 开启全屏模式
*/
enableFullScreenMode() {
if (this.isFullScreenMode) {
return;
}
ALL_PANEL_TYPES.forEach(type => {
if (type !== LayoutPanelType.MAIN_PANEL) {
const panel = this.shell.getPanelFromArea(type);
this.prevPanelMap.set(type, panel.isHidden);
panel.hide();
}
});
this.isFullScreenMode = true;
this.onFullScreenModeChangeEmitter.fire(true);
}
/**
* 关闭全屏模式
*/
disableFullScreenMode() {
if (!this.isFullScreenMode) {
return;
}
ALL_PANEL_TYPES.forEach(type => {
if (type !== LayoutPanelType.MAIN_PANEL) {
const panel = this.shell.getPanelFromArea(type);
const isHidden = Boolean(this.prevPanelMap.get(type));
panel.setHidden(isHidden);
}
});
this.isFullScreenMode = false;
this.onFullScreenModeChangeEmitter.fire(false);
}
/**
* 全屏模式切换
*/
switchFullScreenMode() {
if (!this.isFullScreenMode) {
this.enableFullScreenMode();
} else {
this.disableFullScreenMode();
}
}
/**
* 设置当前 activityBar 激活 item
*/
setActivityBarUri(uri: URI) {
this.shell.activityBarWidget.setCurrentUri(uri);
}
/**
* 获取当前 activityBar 激活的 item
*/
get activityBarUri() {
return this.shell.activityBarWidget.currentUri;
}
}