* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import type { Theme } from '@emotion/react';
import styled from '@emotion/styled';
import { observer } from 'mobx-react';
import React, { useEffect, useMemo, useRef } from 'react';
import { observable, runInAction } from 'mobx';
import { Resizor } from '@insight/lib/resize';
import { useWatchResize } from '../../utils/useWatchDomResize';
import type { Session } from '../../entity/session';
import { ChartRow, type ChartRowProps } from '../base/ChartRow';
import { ChartInteractor } from '../charts/ChartInteractor';
import { ContextMenu } from '../ContextMenu';
import { PinnedUnits } from './PinnedUnits';
import { RefUnits } from './Units';
import ChartHeader from './ChartHeader';
import HorizontalScroller from './HorizontalScrollbar';
import type { ChartInteractorHandles, InteractorMouseState } from '../charts/ChartInteractor/ChartInteractor';
import type { ExtendPos, Pos } from '../charts/ChartInteractor/common';
import { THUMB_WIDTH_PX } from '../base';
import { MouseDownActionResult } from '../charts/ChartInteractor/actions';
import { loopActionFactory } from '../../utils/FactoryActions';
import { RenderEngineContext } from '../../context/context';
import { renderEngine } from '../../renderEngine';
import { DragDirection, useDraggableContainerEx } from '../../utils/useDraggableContainerEx';
import { ActionManager } from '../../actions/manager';
import KeyInfoTooltip from './KeyInfoTooltip';
import FloatingToolbar from './FloatingToolbar';
import { isMac } from '../../utils/is';
const DEFAULT_LANE_INFO_WIDTH = 250;
const DEFAULT_LANE_CHART_WIDTH = 100;
export const TIME_LINE_AXIS_HEIGHT_PX = 30;
const LANE_INFO_WIDTH_PX = observable({ value: DEFAULT_LANE_INFO_WIDTH });
export const CHARTINTERACTOR_NAME = 'chartInteractor';
const Container = styled.div`
flex-grow: 1;
height: 100%;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
background-color: ${(props): string => props.theme.bgColor};
border-radius: ${(props): string => props.theme.borderRadiusBase};
.mask {
position: absolute;
}
/* container header - timeline axis area */
.timeStamp {
display: flex;
background-color: ${(props): string => props.theme.contentBackgroundColor};
height: ${TIME_LINE_AXIS_HEIGHT_PX}px;
}
.line {
position: absolute;
bottom: 0;
width: 100%;
height: 1px;
}
`;
interface Props {
session: Session;
actionManager: ActionManager;
interactive?: boolean;
theme?: Theme;
}
export const Overlay = styled(({ leftOffset, ...props }: { leftOffset: number } & Omit<ChartRowProps, 'leftWidth'>) => (<ChartRow leftWidth={leftOffset} {...props} />))`
width: 100%;
height: 100%;
position: absolute;
top:0;
left:0;
right:0;
bottom:0;
pointer-events: none;
`;
interface ChartBodyProps {
session: Session;
interactive?: boolean;
interactorMouseState: InteractorMouseState;
chartInteractorRef: React.RefObject<ChartInteractorHandles>;
mKeyRange: number[];
}
const ChartBody = observer((props: ChartBodyProps) => {
const { session, interactive, interactorMouseState, chartInteractorRef } = props;
const { domainStart, domainEnd } = session.domainRange;
const [height, ref] = useWatchResize<HTMLDivElement>('height');
const [pinnedHeight, pinnedRef] = useWatchResize<HTMLDivElement>('height');
const splitLineRef = React.useRef<HTMLDivElement>(null);
const [view, handleSwitchOpen] = useDraggableContainerEx(
{ draggableWH: 100, dragDirection: DragDirection.TOP, splitLineRef, open: session.pinnedUnits.length > 0, session });
useEffect(() => {
if (session.pinnedUnits.length > 0) {
handleSwitchOpen(true);
} else {
handleSwitchOpen(false);
}
runInAction(() => {
session.renderTrigger = !session.renderTrigger;
});
}, [session.pinnedUnits]);
useEffect(() => {
runInAction(() => {
session.renderTrigger = !session.renderTrigger;
});
}, [session.selectedUnitKeys]);
return (<>
{
view({
mainContainer: <RefUnits session={session} height={height} ref={ref}
hasPinButton={Boolean(interactive)} laneInfoWidth={LANE_INFO_WIDTH_PX.value} />,
draggableContainer: <PinnedUnits session={session} height={pinnedHeight} ref={pinnedRef} laneInfoWidth={LANE_INFO_WIDTH_PX.value} />,
id: 'UnitsPage',
gap: true,
})
}
<Overlay leftOffset={LANE_INFO_WIDTH_PX.value} rightAreaName={CHARTINTERACTOR_NAME}>
<><Resizor style={{ width: '7px', right: '1px', pointerEvents: 'all' }} onResize={(deltaX: number, width: number, nextWidth?: number): void => {
runInAction(() => {
if (width > DEFAULT_LANE_INFO_WIDTH && nextWidth != null && nextWidth > DEFAULT_LANE_CHART_WIDTH) {
LANE_INFO_WIDTH_PX.value = width;
}
});
}}/> </>
<ChartInteractor ref={chartInteractorRef} splitLineRef={splitLineRef} domainStart={domainStart}
domainEnd={domainEnd} endTimeAll={session.endTimeAll}
interactorMouseState={interactorMouseState} isNsMode={session.isNsMode} session={session}
renderTrigger={session.renderTrigger} scrollTop={session.scrollTop} selectedRange={session.selectedRange}
/>
</Overlay>
<ContextMenu session={session} interactorMouseState={interactorMouseState} chartInteractorRef={chartInteractorRef} />
</>);
});
export const ChartContainer = observer((props: Props) => {
const { session, actionManager } = props;
const [containerDom, setContainerDom] = React.useState<HTMLDivElement | undefined>(undefined);
const chartInteractorRef = useRef<ChartInteractorHandles>(null);
const scrollerRef = React.useRef<HTMLDivElement>(null);
const { onMouseUp, onKeyDown, interactorMouseState, ...otherInteractors } =
useInteractorMouseState(chartInteractorRef, scrollerRef, session, !!props.interactive);
useEffect(() => {
if (containerDom === undefined) {
return (): void => {};
}
document.addEventListener('mouseup', onMouseUp);
return (): void => {
document.removeEventListener('mouseup', onMouseUp);
};
}, [containerDom]);
const keyHoldAction = useMemo(() => loopActionFactory(
(e: React.KeyboardEvent<HTMLDivElement>) => actionManager.handleKeyDown(e, interactorMouseState, chartInteractorRef.current?.xScale), 16, 50),
[session]);
const setPanModifierState = (pressed: boolean): void => {
session.panModePressed = pressed;
};
const handleKeyDownEvent = (e: KeyboardEvent): void => {
if (isPanModifierPressed(e)) {
setPanModifierState(true);
}
if (!e.repeat) {
keyHoldAction.clearLoop();
keyHoldAction.beginLoop(e as unknown as React.KeyboardEvent<HTMLDivElement>);
}
};
const handleKeyUpEvent = (e: KeyboardEvent): void => {
if (!isPanModifierPressed(e)) {
setPanModifierState(false);
}
keyHoldAction.clearLoop();
requestAnimationFrame(() => { actionManager.handleKeyUp(e); });
};
useEffect(() => {
document.addEventListener('keydown', handleKeyDownEvent);
document.addEventListener('keyup', handleKeyUpEvent);
document.addEventListener('blur', keyHoldAction.clearLoop);
return (): void => {
setPanModifierState(false);
document.removeEventListener('keydown', handleKeyDownEvent);
document.removeEventListener('keyup', handleKeyUpEvent);
document.removeEventListener('blur', keyHoldAction.clearLoop);
};
}, []);
return <Container
{...otherInteractors}
ref={(dom): void => {
setContainerDom(dom ?? undefined);
}}
tabIndex={-1}
id={'main-container'}
>
<RenderEngineContext.Provider value={renderEngine}>
<ChartHeader
session={session}
laneInfoWidth={LANE_INFO_WIDTH_PX.value}
timelineHeight={TIME_LINE_AXIS_HEIGHT_PX}
showRecommendation={!props.interactive}
/>
<ChartBody session={session} interactive={props.interactive} interactorMouseState={interactorMouseState}
chartInteractorRef={chartInteractorRef} mKeyRange = {session.mMaskRange}/>
</RenderEngineContext.Provider>
<HorizontalScroller
session={session}
leftLaneInfoWidth={LANE_INFO_WIDTH_PX.value}
containerDom={containerDom}
scrollerRef={scrollerRef}
/>
<FloatingToolbar session={session} />
<KeyInfoTooltip session={session} />
</Container>;
});
const isMouseOnScrollbar = (e: React.MouseEvent, horizontalScroller: HTMLElement | null): boolean => {
const target = e.target as HTMLElement;
const maxClientX = target.getBoundingClientRect().right;
return (e.clientX >= maxClientX - THUMB_WIDTH_PX) || (target === horizontalScroller);
};
function isTargetElement(event: React.MouseEvent): boolean {
let ele: HTMLElement | null = event.target as HTMLElement;
while (ele !== null && ele !== undefined && ele?.id !== 'root') {
ele = ele.parentElement;
}
return Boolean(ele);
}
function isPanModifierPressed(event: KeyboardEvent): boolean {
return isMac ? event.metaKey : event.ctrlKey;
}
const useInteractorMouseState = (chartInteractorRef: React.RefObject<ChartInteractorHandles>, scrollerRef: React.RefObject<HTMLDivElement>,
session: Session, interactive?: boolean): InteractorMouseHandlers => {
const clickPos = useRef<undefined | ExtendPos>(undefined);
const lastPos = useRef<Pos | undefined>(undefined);
const [interactorMouseState, setInteractorMouseState] = React.useState<InteractorMouseState>({ clickPos, lastPos });
const onMouseMove = (e: React.MouseEvent): void => {
if (!chartInteractorRef.current) { return; }
const rect = e.currentTarget.getBoundingClientRect();
const offsetX = e.nativeEvent.x - rect.left - LANE_INFO_WIDTH_PX.value;
const offsetY = e.nativeEvent.y - rect.top;
if (offsetX <= 0) {
interactorMouseState.lastPos.current = interactorMouseState.clickPos.current
? { x: 0, y: offsetY, absoluteX: e.nativeEvent.x, absoluteY: e.nativeEvent.y }
: undefined;
} else {
interactorMouseState.lastPos.current = { x: offsetX, y: offsetY, absoluteX: e.nativeEvent.x, absoluteY: e.nativeEvent.y };
}
chartInteractorRef.current.mouseMoveAction(interactorMouseState, e);
};
const onMouseDown = (e: React.MouseEvent): void => {
const disabled = !isTargetElement(e) || !chartInteractorRef.current || !interactive ||
session.phase !== 'download' || isMouseOnScrollbar(e, scrollerRef.current);
if (disabled) {
interactorMouseState.lastPos.current = undefined;
return;
}
const needDragOneSide = chartInteractorRef.current.mouseDownAction(interactorMouseState, e);
if (needDragOneSide === MouseDownActionResult.NEED_DRAG_ONE_SIDE) {
onMouseMove(e);
}
};
const onWheel = (e: React.WheelEvent<HTMLDivElement>): void => {
if (!chartInteractorRef.current) { return; }
chartInteractorRef.current.mouseWheelAction(interactorMouseState);
setInteractorMouseState({ ...interactorMouseState, wheelEvent: { ctrlKey: e.ctrlKey, deltaY: e.deltaY } });
};
const onMouseUp = (e: MouseEvent): void => {
if (!chartInteractorRef.current || !interactive) { return; }
chartInteractorRef.current.mouseUpAction(interactorMouseState, e);
if (session.sliceSelection.active) { session.sliceSelection.selecting = false; }
};
const onMouseLeave = (): void => {
if (!chartInteractorRef.current) { return; }
chartInteractorRef.current.mouseLeaveAction(interactorMouseState);
};
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>): void => chartInteractorRef.current?.keyDownAction(e, interactorMouseState);
return { onMouseMove, onMouseDown, onWheel, onMouseLeave, onMouseUp, onKeyDown, interactorMouseState };
};
interface InteractorMouseHandlers {
onMouseUp: (e: MouseEvent) => void;
onMouseMove: (e: React.MouseEvent) => void;
onMouseLeave: () => void;
onMouseDown: (e: React.MouseEvent) => void;
onWheel: (e: React.WheelEvent<HTMLDivElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void;
interactorMouseState: InteractorMouseState;
}