Skip to content

Commit

Permalink
[2289] Use layout strategy to layout icon label nodes
Browse files Browse the repository at this point in the history
Bug: #2289
Signed-off-by: Guillaume Coutable <guillaume.coutable@obeo.fr>
  • Loading branch information
gcoutable committed Sep 18, 2023
1 parent 4e0beb6 commit aef306f
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 74 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Expand Up @@ -122,6 +122,7 @@ To illustrate this new feature, we contribute a new tool on the _Papaya Diagram_
- https://github.com/eclipse-sirius/sirius-web/issues/2365[#2365] [diagram] Support key down and F2 to enable direct edit on edges on react-flow diagrams.
- https://github.com/eclipse-sirius/sirius-web/issues/2358[#2358] [form] Adds diagnostic messages to the reference widget.
- https://github.com/eclipse-sirius/sirius-web/issues/2288[#2288] [diagram] Make IconLabel a react flow node
- https://github.com/eclipse-sirius/sirius-web/issues/2289[#2289] [diagram] Use the layout strategy to layout icon label nodes

== v2023.8.0

Expand Down
Expand Up @@ -44,10 +44,21 @@ type Node {
position: Position!
state: ViewModifier!
style: INodeStyle!
childrenLayoutStrategy: ILayoutStrategy
borderNodes: [Node!]!
childNodes: [Node!]!
}

type FreeFormLayoutStrategy {
kind: String!
}

type ListLayoutStrategy {
kind: String!
}

union ILayoutStrategy = FreeFormLayoutStrategy | ListLayoutStrategy

type Label {
id: ID!
text: String!
Expand Down
Expand Up @@ -278,9 +278,7 @@ const toImageNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node<Imag

const convertNode = (gqlNode: GQLNode, parentNode: GQLNode | null, nodes: Node[]): void => {
if (gqlNode.style.__typename === 'RectangularNodeStyle') {
const isList =
(gqlNode.childNodes ?? []).filter((gqlChildNode) => gqlChildNode.style.__typename === 'IconLabelNodeStyle')
.length > 0;
const isList = gqlNode.childrenLayoutStrategy?.kind === 'List';
if (!isList) {
nodes.push(toRectangularNode(gqlNode, parentNode));

Expand Down
Expand Up @@ -44,6 +44,15 @@ fragment nodeFragment on Node {
backgroundColor
}
}
childrenLayoutStrategy {
__typename
... on ListLayoutStrategy {
kind
}
... on FreeFormLayoutStrategy {
kind
}
}
userResizable
}
`;
Expand Up @@ -22,12 +22,26 @@ export interface GQLNode {
state: GQLViewModifier;
label: GQLLabel;
style: GQLNodeStyle;
childrenLayoutStrategy?: ILayoutStrategy;
borderNodes: GQLNode[] | undefined;
childNodes: GQLNode[] | undefined;
position: GQLPosition;
size: GQLSize;
}

export interface ILayoutStrategy {
__typename: string;
kind: string;
}

export interface ListLayoutStrategy extends ILayoutStrategy {
kind: 'List';
}

export interface FreeFormLayoutStrategy extends ILayoutStrategy {
kind: 'FreeForm';
}

export enum GQLViewModifier {
Normal = 'Normal',
Faded = 'Faded',
Expand Down
Expand Up @@ -27,6 +27,15 @@ import { ImageNodeData } from '../node/ImageNode.types';
import { ListNodeData } from '../node/ListNode.types';
import { RectangularNodeData } from '../node/RectangularNode.types';

const isListNode = (node: Node<NodeData>): node is Node<ListNodeData> => node.type === 'listNode';
const isRectangularNode = (node: Node<NodeData>): node is Node<RectangularNodeData> => node.type === 'rectangularNode';
const isImageNode = (node: Node<NodeData>): node is Node<ImageNodeData> => node.type === 'imageNode';
const isIconLabelNode = (node: Node<NodeData>): node is Node<IconLabelNodeData> => node.type === 'iconLabelNode';
const getBorderWidth = (style: React.CSSProperties): number => {
const borderWidth = Number(style.borderWidth);
return !Number.isNaN(borderWidth) ? borderWidth : 0; // WARN: Works only if the value is in pixel. It may need to pre-compute the layout to have the pixel value of the computed style
};

const elk = new ELK();

export const prepareLayoutArea = (diagram: Diagram, renderCallback: () => void, httpOrigin: string): HTMLDivElement => {
Expand Down Expand Up @@ -120,16 +129,11 @@ export const layout = (diagram: Diagram): Diagram => {

const findNodeIndex = (nodes: Node[], refNodeId: string): number => nodes.findIndex((node) => node.id === refNodeId);

const isListNode = (node: Node<NodeData>): node is Node<ListNodeData> => node.type === 'listNode';
const isRectangularNode = (node: Node<NodeData>): node is Node<RectangularNodeData> => node.type === 'rectangularNode';
const isImageNode = (node: Node<NodeData>): node is Node<ImageNodeData> => node.type === 'imageNode';
const isIconLabelNode = (node: Node<NodeData>): node is Node<IconLabelNodeData> => node.type === 'iconLabelNode';

const layoutDiagram = (diagram: Diagram) => {
const allVisibleNodes = diagram.nodes.filter((node) => !node.hidden);
const nodesToLayout = allVisibleNodes.filter((node) => !node.parentNode);

layoutNodes(allVisibleNodes, nodesToLayout);
layoutNodes(allVisibleNodes, nodesToLayout, null);
// Update position of root nodes
nodesToLayout.forEach((rootNode, index) => {
rootNode.position = { x: 0, y: 0 };
Expand All @@ -140,13 +144,13 @@ const layoutDiagram = (diagram: Diagram) => {
});
};

const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node<NodeData>[]) => {
const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node<NodeData>[], forceWidth: number | null) => {
nodesToLayout.forEach((nodeToLayout) => {
const directChildren = allVisibleNodes.filter((node) => node.parentNode === nodeToLayout.id);
if (isRectangularNode(nodeToLayout)) {
const borderWidth = (nodeToLayout.data.style.borderWidth as number) ?? 0; // WARN: Works only if the value is in pixel. It may need to pre-compute the layout to have the pixel value of the computed style
const borderWidth = getBorderWidth(nodeToLayout.data.style);
if (directChildren.length > 0) {
layoutNodes(allVisibleNodes, directChildren);
layoutNodes(allVisibleNodes, directChildren, null);

const labelElement = document.getElementById(
`${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}`
Expand All @@ -155,8 +159,12 @@ const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node<NodeData>[]) =
// Update children position to be under the label and at the right padding.
directChildren.forEach((child, index) => {
child.position = {
x: rectangularNodePadding,
y: rectangularNodePadding + (labelElement?.getBoundingClientRect().height ?? 0) + rectangularNodePadding,
x: borderWidth + rectangularNodePadding,
y:
borderWidth +
rectangularNodePadding +
(labelElement?.getBoundingClientRect().height ?? 0) +
rectangularNodePadding,
};
const previousSibling = directChildren[index - 1];
if (previousSibling) {
Expand All @@ -167,19 +175,18 @@ const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node<NodeData>[]) =
// Update node to layout size
// WARN: We suppose label are always on top of children (that wrong)
const childrenFootprint = getChildrenFootprint(directChildren);
const childrenAwareNodeWidth = childrenFootprint.x + childrenFootprint.width + rectangularNodePadding;
const labelOnlyWidth =
rectangularNodePadding + (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding;
const nodeWidth = Math.max(childrenAwareNodeWidth, labelOnlyWidth) + borderWidth * 2;
nodeToLayout.width = getNodeOrMinWidth(nodeWidth);
nodeToLayout.height = getNodeOrMinHeight(
const labelOnlyWidth = labelElement?.getBoundingClientRect().width ?? 0;
const nodeWidth =
Math.max(childrenFootprint.width, labelOnlyWidth) + rectangularNodePadding * 2 + borderWidth * 2;
const nodeHeight =
rectangularNodePadding +
(labelElement?.getBoundingClientRect().height ?? 0) +
rectangularNodePadding +
childrenFootprint.height +
rectangularNodePadding +
borderWidth * 2
);
(labelElement?.getBoundingClientRect().height ?? 0) +
rectangularNodePadding +
childrenFootprint.height +
rectangularNodePadding +
borderWidth * 2;
nodeToLayout.width = forceWidth ?? getNodeOrMinWidth(nodeWidth);
nodeToLayout.height = getNodeOrMinHeight(nodeHeight);
} else {
const labelElement = document.getElementById(
`${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}`
Expand All @@ -190,68 +197,67 @@ const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node<NodeData>[]) =
(labelElement?.getBoundingClientRect().width ?? 0) +
rectangularNodePadding +
borderWidth * 2;

const labelHeight =
rectangularNodePadding + (labelElement?.getBoundingClientRect().height ?? 0) + rectangularNodePadding;
const nodeWidth = getNodeOrMinWidth(labelWidth);
const nodeHeight = getNodeOrMinHeight(labelHeight);
nodeToLayout.width = nodeWidth;
nodeToLayout.height = nodeHeight;
nodeToLayout.width = forceWidth ?? getNodeOrMinWidth(labelWidth);
nodeToLayout.height = getNodeOrMinHeight(labelHeight);
}
} else if (isImageNode(nodeToLayout)) {
nodeToLayout.width = defaultWidth;
nodeToLayout.width = forceWidth ?? defaultWidth;
nodeToLayout.height = defaultHeight;
} else if (isListNode(nodeToLayout)) {
const borderWidth = getBorderWidth(nodeToLayout.data.style);
if (directChildren.length > 0) {
layoutNodes(allVisibleNodes, directChildren);
layoutNodes(allVisibleNodes, directChildren, forceWidth);
const labelElement = document.getElementById(
`${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}`
);

const iconLabelNodes = directChildren.filter(isIconLabelNode);
iconLabelNodes.forEach((child, index) => {
if (!forceWidth) {
const widerWidth = directChildren.reduce<number>(
(widerWidth, child) => Math.max(child.width ?? 0, widerWidth),
labelElement?.getBoundingClientRect().width ?? 0
);

layoutNodes(allVisibleNodes, directChildren, widerWidth);
}

directChildren.forEach((child, index) => {
child.position = {
x: 0,
y: (labelElement?.getBoundingClientRect().height ?? 0) + rectangularNodePadding,
x: borderWidth,
y: borderWidth + (labelElement?.getBoundingClientRect().height ?? 0),
};
const previousSibling = iconLabelNodes[index - 1];
const previousSibling = directChildren[index - 1];
if (previousSibling) {
child.position = { ...child.position, y: previousSibling.position.y + (previousSibling.height ?? 0) };
}
});

const childrenFootprint = getChildrenFootprint(iconLabelNodes);
const childrenAwareNodeWidth = childrenFootprint.x + childrenFootprint.width + rectangularNodePadding;
const labelOnlyWidth =
rectangularNodePadding + (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding;
const borderWidth = (nodeToLayout.data.style.borderWidth as number) ?? 0; // WARN: Works only if the value is in pixel. It may need to pre-compute the layout to have the pixel value of the computed style
const nodeWidth = Math.max(childrenAwareNodeWidth, labelOnlyWidth) + borderWidth * 2;
nodeToLayout.width = getNodeOrMinWidth(nodeWidth);
nodeToLayout.height = getNodeOrMinHeight(
(labelElement?.getBoundingClientRect().height ?? 0) +
rectangularNodePadding +
childrenFootprint.height +
borderWidth * 2
const childrenFootprint = getChildrenFootprint(directChildren);
const labelOnlyWidth = labelElement?.getBoundingClientRect().width ?? 0;
const nodeWidth = Math.max(childrenFootprint.width, labelOnlyWidth) + borderWidth * 2;
nodeToLayout.width = forceWidth ?? getNodeOrMinWidth(nodeWidth);
nodeToLayout.height =
getNodeOrMinHeight((labelElement?.getBoundingClientRect().height ?? 0) + childrenFootprint.height) +
borderWidth * 2;
} else {
const labelElement = document.getElementById(
`${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}`
);

if (nodeWidth > childrenAwareNodeWidth) {
// we need to adjust the width of children
iconLabelNodes.forEach((child) => {
child.width = nodeWidth;
child.style = {
...child.style,
width: `${nodeWidth}px`,
};
});
}
} else {
nodeToLayout.width = getNodeOrMinWidth(undefined);
nodeToLayout.height = getNodeOrMinHeight(undefined);
const nodeWidth = (labelElement?.getBoundingClientRect().width ?? 0) + borderWidth * 2;
const nodeHeight = (labelElement?.getBoundingClientRect().height ?? 0) + borderWidth * 2;
nodeToLayout.width = forceWidth ?? getNodeOrMinWidth(nodeWidth);
nodeToLayout.height = getNodeOrMinHeight(nodeHeight);
}
} else if (isIconLabelNode(nodeToLayout)) {
const labelElement = document.getElementById(
`${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}`
);
nodeToLayout.width = labelElement?.getBoundingClientRect().width;
nodeToLayout.width =
forceWidth ??
rectangularNodePadding + (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding;
nodeToLayout.height = labelElement?.getBoundingClientRect().height;
}
nodeToLayout.style = {
Expand Down Expand Up @@ -297,7 +303,10 @@ const getBoundsOfBoxes = (box1: Box, box2: Box): Box => {
};
};

export const performDefaultAutoLayout = (nodes: Node[], edges: Edge[]): Promise<{ nodes: Node[] }> => {
export const performDefaultAutoLayout = (
nodes: Node<NodeData>[],
edges: Edge[]
): Promise<{ nodes: Node<NodeData>[] }> => {
const layoutOptions: LayoutOptions = {
'elk.algorithm': 'layered',
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
Expand All @@ -311,10 +320,10 @@ export const performDefaultAutoLayout = (nodes: Node[], edges: Edge[]): Promise<
};

export const performAutoLayout = (
nodes: Node[],
nodes: Node<NodeData>[],
edges: Edge[],
layoutOptions: LayoutOptions
): Promise<{ nodes: Node[] }> => {
): Promise<{ nodes: Node<NodeData>[] }> => {
const graph: ElkNode = {
id: 'root',
layoutOptions,
Expand All @@ -335,8 +344,8 @@ export const performAutoLayout = (
'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]',
},
};
if (node.type === 'rectangularNode') {
const rectangularNodeData: RectangularNodeData = node.data as RectangularNodeData;
if (isRectangularNode(node)) {
const rectangularNodeData: RectangularNodeData = node.data;

const label = document.querySelector<HTMLDivElement>(`[data-id="${rectangularNodeData.label?.id}"]`);
if (label) {
Expand Down
Expand Up @@ -12,15 +12,15 @@
*******************************************************************************/

import { Edge, Node } from 'reactflow';
import { Diagram } from '../DiagramRenderer.types';
import { Diagram, NodeData } from '../DiagramRenderer.types';

export interface UseLayoutValue {
layout: (
previousLaidoutDiagram: Diagram | null,
diagramToLayout: Diagram,
callback: (laidoutDiagram: Diagram) => void
) => void;
autoLayout: (nodes: Node[], edges: Edge[]) => Promise<{ nodes: Node[] }>;
autoLayout: (nodes: Node<NodeData>[], edges: Edge[]) => Promise<{ nodes: Node<NodeData>[] }>;
}

export type Step = 'INITIAL_STEP' | 'BEFORE_LAYOUT' | 'LAYOUT' | 'AFTER_LAYOUT';
Expand Down
Expand Up @@ -26,7 +26,6 @@ const iconlabelStyle = (
): React.CSSProperties => {
const iconLabelNodeStyle: React.CSSProperties = {
opacity: faded ? '0.4' : '',
marginLeft: '8px',
...style,
};

Expand All @@ -40,9 +39,11 @@ const iconlabelStyle = (
export const IconLabelNode = memo(({ data, id, selected }: NodeProps<IconLabelNodeData>) => {
const theme = useTheme();
return (
<div style={iconlabelStyle(data.style, theme, selected, data.faded)}>
{data.label ? <Label diagramElementId={id} label={data.label} faded={data.faded} transform="" /> : null}
{selected ? <NodePalette diagramElementId={id} labelId={data?.label?.id ?? null} /> : null}
<div style={{ paddingLeft: '8px', paddingRight: '8px' }}>
<div style={iconlabelStyle(data.style, theme, selected, data.faded)}>
{data.label ? <Label diagramElementId={id} label={data.label} faded={data.faded} transform="" /> : null}
{selected ? <NodePalette diagramElementId={id} labelId={data?.label?.id ?? null} /> : null}
</div>
</div>
);
});

0 comments on commit aef306f

Please sign in to comment.