Merged PR 328: #7973!! Implement Highlight on symbols

Implement Highlight on symbols

Related work items: #7973
This commit is contained in:
Carl Fuchs 2023-02-13 15:38:26 +00:00 committed by Eric Nguyen
commit 30e94f6d92
9 changed files with 203 additions and 77 deletions

View file

@ -53,6 +53,7 @@ export function SaveEditorAsSVG(): void {
svg.replaceChildren(...mainSvg); svg.replaceChildren(...mainSvg);
// remove the selector // remove the selector
// TODO: Fix this with SelectorMode != Nothing or with some html magic
const group = svg.children[svg.children.length - 1]; const group = svg.children[svg.children.length - 1];
group.removeChild(group.children[group.children.length - 1]); group.removeChild(group.children[group.children.length - 1]);
if (SHOW_SELECTOR_TEXT) { if (SHOW_SELECTOR_TEXT) {

View file

@ -1,38 +1,18 @@
import './Selector.scss'; import '../Selector.scss';
import * as React from 'react'; import * as React from 'react';
import { IContainerModel } from '../../../../Interfaces/IContainerModel';
import { SHOW_SELECTOR_TEXT } from '../../../../utils/default'; import { SHOW_SELECTOR_TEXT } from '../../../../utils/default';
import { GetAbsolutePosition } from '../../../../utils/itertools';
import { RemoveMargin } from '../../../../utils/svg';
interface ISelectorProps { interface ISelectorProps {
containers: Map<string, IContainerModel> text: string
selected?: IContainerModel x: number
scale?: number y: number
width: number
height: number
scale: number
style?: React.CSSProperties
} }
export function Selector(props: ISelectorProps): JSX.Element { export function Selector({ text, x, y, width, height, scale, style: overrideStyle }: ISelectorProps): JSX.Element {
if (props.selected === undefined || props.selected === null) {
return (
<rect visibility={'hidden'}>
</rect>
);
}
const scale = (props.scale ?? 1);
let [x, y] = GetAbsolutePosition(props.containers, props.selected);
let [width, height] = [
props.selected.properties.width,
props.selected.properties.height
];
({ x, y, width, height } = RemoveMargin(x, y, width, height,
props.selected.properties.margin.left,
props.selected.properties.margin.bottom,
props.selected.properties.margin.top,
props.selected.properties.margin.right
));
const xText = x + width / 2; const xText = x + width / 2;
const yText = y + height / 2; const yText = y + height / 2;
@ -43,7 +23,8 @@ export function Selector(props: ISelectorProps): JSX.Element {
transitionProperty: 'all', transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms', transitionDuration: '150ms',
animation: 'fadein 750ms ease-in alternate infinite' animation: 'fadein 750ms ease-in alternate infinite',
...overrideStyle
}; };
return ( return (
@ -65,7 +46,7 @@ export function Selector(props: ISelectorProps): JSX.Element {
transformBox: 'fill-box' transformBox: 'fill-box'
}} }}
> >
{props.selected.properties.displayedText} { text }
</text> </text>
: null} : null}
</> </>

View file

@ -0,0 +1,46 @@
import '../Selector.scss';
import * as React from 'react';
import { type IContainerModel } from '../../../../Interfaces/IContainerModel';
import { GetAbsolutePosition } from '../../../../utils/itertools';
import { RemoveMargin } from '../../../../utils/svg';
import { Selector } from '../Selector/Selector';
interface ISelectorContainerProps {
containers: Map<string, IContainerModel>
selected?: IContainerModel
scale?: number
}
export function SelectorContainer(props: ISelectorContainerProps): JSX.Element {
if (props.selected === undefined || props.selected === null) {
return (
<rect visibility={'hidden'}>
</rect>
);
}
const scale = (props.scale ?? 1);
let [x, y] = GetAbsolutePosition(props.containers, props.selected);
let [width, height] = [
props.selected.properties.width,
props.selected.properties.height
];
({ x, y, width, height } = RemoveMargin(x, y, width, height,
props.selected.properties.margin.left,
props.selected.properties.margin.bottom,
props.selected.properties.margin.top,
props.selected.properties.margin.right
));
return (
<Selector
text={props.selected.properties.displayedText}
x={x}
y={y}
width={width}
height={height}
scale={scale}
/>
);
}

View file

@ -0,0 +1,47 @@
import '../Selector.scss';
import * as React from 'react';
import { SYMBOL_MARGIN } from '../../../../utils/default';
import { type ISymbolModel } from '../../../../Interfaces/ISymbolModel';
import { Selector } from '../Selector/Selector';
interface ISelectorSymbolProps {
symbols: Map<string, ISymbolModel>
selected?: ISymbolModel
scale?: number
}
export function SelectorSymbol(props: ISelectorSymbolProps): JSX.Element {
if (props.selected === undefined || props.selected === null) {
return (
<rect visibility={'hidden'}>
</rect>
);
}
const scale = (props.scale ?? 1);
const [width, height] = [
props.selected.width / scale,
props.selected.height / scale
];
const [x, y] = [
props.selected.x + props.selected.width / 2,
-SYMBOL_MARGIN - height];
const style: React.CSSProperties = {
transform: 'translateX(-50%)',
transformBox: 'fill-box'
};
return (
<Selector
text={props.selected.displayedText}
x={x}
y={y}
width={width}
height={height}
scale={scale}
style={style}
/>
);
}

View file

@ -1,12 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { ReactSVGPanZoom, Tool, TOOL_PAN, Value } from 'react-svg-pan-zoom'; import { ReactSVGPanZoom, type Tool, TOOL_PAN, type Value } from 'react-svg-pan-zoom';
import { Container } from './Elements/Container'; import { Container } from './Elements/Container';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel } from '../../Interfaces/IContainerModel';
import { Selector } from './Elements/Selector/Selector'; import { SelectorContainer } from './Elements/SelectorContainer/SelectorContainer';
import { MAX_FRAMERATE } from '../../utils/default'; import { MAX_FRAMERATE } from '../../utils/default';
import { SymbolLayer } from './Elements/SymbolLayer'; import { SymbolLayer } from './Elements/SymbolLayer';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
import { DimensionLayer } from './Elements/DimensionLayer'; import { DimensionLayer } from './Elements/DimensionLayer';
import { SelectorSymbol } from './Elements/SelectorSymbol/SelectorSymbol';
interface ISVGProps { interface ISVGProps {
className?: string className?: string
@ -16,11 +17,19 @@ interface ISVGProps {
height: number height: number
containers: Map<string, IContainerModel> containers: Map<string, IContainerModel>
children: IContainerModel children: IContainerModel
selected?: IContainerModel selectedContainer?: IContainerModel
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
selectedSymbol?: ISymbolModel
selectorMode: SelectorMode
selectContainer: (containerId: string) => void selectContainer: (containerId: string) => void
} }
export enum SelectorMode {
Nothing,
Containers,
Symbols
}
export const ID = 'svg'; export const ID = 'svg';
export function SVG(props: ISVGProps): JSX.Element { export function SVG(props: ISVGProps): JSX.Element {
@ -48,8 +57,7 @@ export function SVG(props: ISVGProps): JSX.Element {
xmlns xmlns
}; };
let children: React.ReactNode | React.ReactNode[] = []; const children: React.ReactNode | React.ReactNode[] = <Container
children = <Container
key={`container-${props.children.properties.id}`} key={`container-${props.children.properties.id}`}
containers={props.containers} containers={props.containers}
model={props.children} model={props.children}
@ -58,6 +66,25 @@ export function SVG(props: ISVGProps): JSX.Element {
selectContainer={props.selectContainer} selectContainer={props.selectContainer}
/>; />;
function Selector(): JSX.Element {
switch (props.selectorMode) {
case SelectorMode.Containers:
return <SelectorContainer
containers={props.containers}
scale={scale}
selected={props.selectedContainer}
/>;
case SelectorMode.Symbols:
return <SelectorSymbol
symbols={props.symbols}
scale={scale}
selected={props.selectedSymbol}
/>;
default:
return <></>;
}
}
return ( return (
<div id={ID} className={props.className}> <div id={ID} className={props.className}>
<ReactSVGPanZoom <ReactSVGPanZoom
@ -95,7 +122,7 @@ export function SVG(props: ISVGProps): JSX.Element {
{children} {children}
<DimensionLayer containers={props.containers} symbols={props.symbols} scale={scale} root={props.children} /> <DimensionLayer containers={props.containers} symbols={props.symbols} scale={scale} root={props.children} />
<SymbolLayer scale={scale} symbols={props.symbols} /> <SymbolLayer scale={scale} symbols={props.symbols} />
<Selector containers={props.containers} scale={scale} selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */} <Selector />
</svg> </svg>
</ReactSVGPanZoom> </ReactSVGPanZoom>
</div> </div>

View file

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import useSize from '@react-hook/size'; import useSize from '@react-hook/size';
import { FixedSizeList as List } from 'react-window'; import { FixedSizeList as List } from 'react-window';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
import { SymbolProperties } from '../SymbolProperties/SymbolProperties'; import { SymbolProperties } from '../SymbolProperties/SymbolProperties';
import { ToggleSideBar } from '../Sidebar/ToggleSideBar/ToggleSideBar'; import { ToggleSideBar } from '../Sidebar/ToggleSideBar/ToggleSideBar';
import { Text } from '../Text/Text'; import { Text } from '../Text/Text';
@ -23,28 +23,7 @@ export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
const [showProperties, setShowProperties] = useState(props.isExpanded); const [showProperties, setShowProperties] = useState(props.isExpanded);
// Render // Render
const symbols = [...props.symbols.values()]; const symbols = [...props.symbols.values()];
function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element {
const symbol = symbols[index];
const key = symbol.id;
const text = symbol.displayedText;
const selectedClass: string = props.selectedSymbolId !== '' &&
props.selectedSymbolId === symbol.id
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300';
return (
<button type="button"
className={`w-full border-blue-500 symbols-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}`}
id={key}
key={key}
style={style}
onClick={() => props.selectSymbol(key)}
>
{text}
</button>
);
}
const selectedSymbol = props.symbols.get(props.selectedSymbolId); const selectedSymbol = props.symbols.get(props.selectedSymbolId);
return ( return (
<div className='flex flex-row h-full w-full'> <div className='flex flex-row h-full w-full'>
@ -71,4 +50,27 @@ export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
</div> </div>
</div> </div>
); );
function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element {
const symbol = symbols[index];
const key = symbol.id;
const text = symbol.displayedText;
const selectedClass: string = props.selectedSymbolId !== '' &&
props.selectedSymbolId === symbol.id
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300';
return (
<button type="button"
className={`w-full border-blue-500 symbols-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}`}
id={key}
key={key}
style={style}
onClick={() => { props.selectSymbol(key); }}
>
{text}
</button>
);
}
} }

View file

@ -4,17 +4,17 @@ import { History } from '../History/History';
import { Bar, BAR_WIDTH } from '../Bar/Bar'; import { Bar, BAR_WIDTH } from '../Bar/Bar';
import { Symbols } from '../Symbols/Symbols'; import { Symbols } from '../Symbols/Symbols';
import { SymbolsSidebar } from '../SymbolsList/SymbolsSidebar'; import { SymbolsSidebar } from '../SymbolsList/SymbolsSidebar';
import { PropertyType } from '../../Enums/PropertyType'; import { type PropertyType } from '../../Enums/PropertyType';
import { Messages } from '../Messages/Messages'; import { Messages } from '../Messages/Messages';
import { Sidebar } from '../Sidebar/Sidebar'; import { Sidebar } from '../Sidebar/Sidebar';
import { Components } from '../Components/Components'; import { Components } from '../Components/Components';
import { Viewer } from '../Viewer/Viewer'; import { Viewer } from '../Viewer/Viewer';
import { Settings } from '../Settings/Settings'; import { Settings } from '../Settings/Settings';
import { IMessage } from '../../Interfaces/IMessage'; import { type IMessage } from '../../Interfaces/IMessage';
import { DISABLE_API } from '../../utils/default'; import { DISABLE_API } from '../../utils/default';
import { UseWorker, UseAsync } from './UseWorker'; import { UseWorker, UseAsync } from './UseWorker';
import { FindContainerById } from '../../utils/itertools'; import { FindContainerById } from '../../utils/itertools';
import { IEditorState } from '../../Interfaces/IEditorState'; import { type IEditorState } from '../../Interfaces/IEditorState';
import { GetCurrentHistoryState } from '../Editor/Editor'; import { GetCurrentHistoryState } from '../Editor/Editor';
import { Text } from '../Text/Text'; import { Text } from '../Text/Text';
@ -96,6 +96,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
} }
const selectedContainer = FindContainerById(current.containers, current.selectedContainerId); const selectedContainer = FindContainerById(current.containers, current.selectedContainerId);
const selectedSymbol = current.symbols.get(current.selectedSymbolId);
switch (selectedSidebar) { switch (selectedSidebar) {
case SidebarType.Components: case SidebarType.Components:
@ -116,7 +117,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
selectContainer={methods.selectContainer} selectContainer={methods.selectContainer}
addContainer={methods.addContainerAt} addContainer={methods.addContainerAt}
isExpanded ={false} isExpanded ={false}
onExpandChange={() => setOrToggleSidebar(SidebarType.ComponentsExpanded) } onExpandChange={() => { setOrToggleSidebar(SidebarType.ComponentsExpanded); } }
/>; />;
break; break;
case SidebarType.ComponentsExpanded: case SidebarType.ComponentsExpanded:
@ -137,7 +138,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
selectContainer={methods.selectContainer} selectContainer={methods.selectContainer}
addContainer={methods.addContainerAt} addContainer={methods.addContainerAt}
isExpanded ={true} isExpanded ={true}
onExpandChange={() => setOrToggleSidebar(SidebarType.Components) } onExpandChange={() => { setOrToggleSidebar(SidebarType.Components); } }
/>; />;
break; break;
case SidebarType.Symbols: case SidebarType.Symbols:
@ -153,7 +154,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
onPropertyChange={methods.onSymbolPropertyChange} onPropertyChange={methods.onSymbolPropertyChange}
selectSymbol={methods.selectSymbol} selectSymbol={methods.selectSymbol}
isExpanded ={false} isExpanded ={false}
onExpandChange={() => setOrToggleSidebar(SidebarType.SymbolsExpanded) } onExpandChange={() => { setOrToggleSidebar(SidebarType.SymbolsExpanded); } }
/>; />;
break; break;
case SidebarType.SymbolsExpanded: case SidebarType.SymbolsExpanded:
@ -169,7 +170,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
onPropertyChange={methods.onSymbolPropertyChange} onPropertyChange={methods.onSymbolPropertyChange}
selectSymbol={methods.selectSymbol} selectSymbol={methods.selectSymbol}
isExpanded ={true} isExpanded ={true}
onExpandChange={() => setOrToggleSidebar(SidebarType.Symbols)} onExpandChange={() => { setOrToggleSidebar(SidebarType.Symbols); }}
/>; />;
break; break;
@ -187,7 +188,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
leftChildren = <Messages leftChildren = <Messages
historyState={current} historyState={current}
messages={messages} messages={messages}
clearMessage={() => setMessages([])} clearMessage={() => { setMessages([]); }}
/>; />;
break; break;
@ -242,11 +243,14 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
isLeftSidebarOpenClasses.add('left-sidebar-single'); isLeftSidebarOpenClasses.add('left-sidebar-single');
} }
const isComponentsOpen = selectedSidebar === SidebarType.Components || selectedSidebar === SidebarType.ComponentsExpanded;
const isSymbolsOpen = selectedSidebar === SidebarType.Symbols || selectedSidebar === SidebarType.SymbolsExpanded;
return ( return (
<> <>
<Bar <Bar
isComponentsOpen={selectedSidebar === SidebarType.Components || selectedSidebar === SidebarType.ComponentsExpanded} isComponentsOpen={isComponentsOpen}
isSymbolsOpen={selectedSidebar === SidebarType.Symbols || selectedSidebar === SidebarType.SymbolsExpanded} isSymbolsOpen={isSymbolsOpen}
isHistoryOpen={selectedSidebar === SidebarType.History} isHistoryOpen={selectedSidebar === SidebarType.History}
isMessagesOpen={selectedSidebar === SidebarType.Messages} isMessagesOpen={selectedSidebar === SidebarType.Messages}
isSettingsOpen={selectedSidebar === SidebarType.Settings} isSettingsOpen={selectedSidebar === SidebarType.Settings}
@ -283,8 +287,11 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
<Viewer <Viewer
className={`${[...viewerMarginClasses.values()].join(' ')} w-full h-full`} className={`${[...viewerMarginClasses.values()].join(' ')} w-full h-full`}
current={current} current={current}
isComponentsOpen={isComponentsOpen}
isSymbolsOpen={isSymbolsOpen}
selectedContainer={selectedContainer} selectedContainer={selectedContainer}
selectContainer={methods.selectContainer} selectContainer={methods.selectContainer}
selectedSymbol={selectedSymbol}
margin={marginSidebar} margin={marginSidebar}
/> />
<Sidebar <Sidebar

View file

@ -1,23 +1,27 @@
import * as React from 'react'; import * as React from 'react';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { type IContainerModel } from '../../Interfaces/IContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState'; import { type IHistoryState } from '../../Interfaces/IHistoryState';
import { IPoint } from '../../Interfaces/IPoint'; import { type IPoint } from '../../Interfaces/IPoint';
import { DIMENSION_MARGIN, USE_EXPERIMENTAL_CANVAS_API } from '../../utils/default'; import { DIMENSION_MARGIN, USE_EXPERIMENTAL_CANVAS_API } from '../../utils/default';
import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools'; import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools';
import { BAR_WIDTH } from '../Bar/Bar'; import { BAR_WIDTH } from '../Bar/Bar';
import { Canvas } from '../Canvas/Canvas'; import { Canvas } from '../Canvas/Canvas';
import { AddDimensions } from '../Canvas/DimensionLayer'; import { AddDimensions } from '../Canvas/DimensionLayer';
import { RenderSelector } from '../Canvas/Selector'; import { RenderSelector } from '../Canvas/Selector';
import { SVG } from '../SVG/SVG'; import { SelectorMode, SVG } from '../SVG/SVG';
import { RenderSymbol } from '../Canvas/Symbol'; import { RenderSymbol } from '../Canvas/Symbol';
import { useState } from 'react'; import { useState } from 'react';
import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
interface IViewerProps { interface IViewerProps {
className?: string className?: string
current: IHistoryState current: IHistoryState
selectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
selectContainer: (containerId: string) => void selectContainer: (containerId: string) => void
selectedSymbol: ISymbolModel | undefined
margin: number margin: number
isComponentsOpen: boolean
isSymbolsOpen: boolean
} }
export function Viewer({ export function Viewer({
@ -25,7 +29,10 @@ export function Viewer({
current, current,
selectedContainer, selectedContainer,
selectContainer, selectContainer,
margin selectedSymbol,
margin,
isComponentsOpen,
isSymbolsOpen
}: IViewerProps): JSX.Element { }: IViewerProps): JSX.Element {
function computeWidth(margin: number): number { function computeWidth(margin: number): number {
return window.innerWidth - (window.innerWidth < 768 ? BAR_WIDTH : margin); return window.innerWidth - (window.innerWidth < 768 ? BAR_WIDTH : margin);
@ -64,6 +71,13 @@ export function Viewer({
return <></>; return <></>;
} }
let selectorMode = SelectorMode.Nothing;
if (isComponentsOpen) {
selectorMode = SelectorMode.Containers;
} else if (isSymbolsOpen) {
selectorMode = SelectorMode.Symbols;
}
if (USE_EXPERIMENTAL_CANVAS_API) { if (USE_EXPERIMENTAL_CANVAS_API) {
function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): void { function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): void {
if (mainContainer === undefined) { if (mainContainer === undefined) {
@ -134,11 +148,12 @@ export function Viewer({
width={mainContainer.properties.width} width={mainContainer.properties.width}
height={mainContainer.properties.height} height={mainContainer.properties.height}
containers={current.containers} containers={current.containers}
selected={selectedContainer} selectedContainer={selectedContainer}
symbols={current.symbols} symbols={current.symbols}
selectedSymbol={selectedSymbol}
selectorMode={selectorMode}
selectContainer={selectContainer} selectContainer={selectContainer}
> >
{mainContainer} {mainContainer}
</SVG> </SVG>
); );