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 Oct 9, 2023
1 parent 305015a commit f92bda7
Show file tree
Hide file tree
Showing 16 changed files with 171 additions and 87 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.adoc
Expand Up @@ -40,7 +40,7 @@ As a result, the identifier of a form description created from a view model will
In order to provide a first version of those capabilities, some additional coupling has been created between the reference widget and its use in Sirius Web which may prevent its use in other applications.
This coupling will be removed in the near future as a consequence, the dependencies of this code will change and some APIs of the reference widget will be broken in the near future.
- 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.10.0

Expand Down
Expand Up @@ -11,6 +11,29 @@
* Obeo - initial API and implementation
*******************************************************************************/
describe('/projects/:projectId/edit - Robot Diagram', () => {
const fadeByElementTestId = (elementTestId) => {
cy.getByTestId(elementTestId).should('have.css', 'opacity', '1');
cy.getByTestId(elementTestId).first().click({ force: true });
cy.wait(200); // wait for the palette to be loaded completely
cy.getByTestId('Fade-elements').should('exist').click({ force: true });
cy.getByTestId(elementTestId).should('have.css', 'opacity', '0.4');
};

const hideByElementTestId = (elementTestId) => {
cy.getByTestId(elementTestId).then((elementBefore) => {
console.log(elementTestId);
const countBefore = elementBefore.length ?? 0;
console.log(countBefore);
cy.getByTestId(elementTestId).first().click({ force: true });
cy.wait(200); // wait for the palette to be loaded completely
cy.getByTestId('Hide-elements').should('exist').click({ force: true });
cy.getByTestId(elementTestId).then((elementAfter) => {
console.log(elementAfter.length ?? 0);
cy.expect((elementAfter.length ?? 0) + 1).to.equal(countBefore);
});
});
};

beforeEach(() => {
cy.deleteAllProjects();
cy.createProject('Cypress Project').then((res) => {
Expand Down Expand Up @@ -42,4 +65,18 @@ describe('/projects/:projectId/edit - Robot Diagram', () => {
cy.getByTestId('Hide-elements').should('not.exist');
cy.getByTestId('Fade-elements').should('not.exist');
});

it('can fade any type of nodes', () => {
fadeByElementTestId('IconLabel - Temperature: 25');
fadeByElementTestId('Image - Motion_Engine');
fadeByElementTestId('Rectangle - Central_Unit');
fadeByElementTestId('List - Description');
});

it.only('can hide any type of nodes', () => {
hideByElementTestId('IconLabel - Temperature: 25');
hideByElementTestId('Image - Motion_Engine');
hideByElementTestId('List - Description');
hideByElementTestId('Rectangle - Central_Unit');
});
});
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 @@ -279,9 +279,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 @@ -16,6 +16,8 @@ import { IconLabelNodeData } from '../node/IconsLabelNode.types';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { ILayoutEngine, INodeLayoutHandler } from './LayoutEngine.types';

const rectangularNodePadding = 8;

export class IconLabelNodeLayoutHandler implements INodeLayoutHandler<IconLabelNodeData> {
canHandle(node: Node<NodeData, DiagramNodeType>) {
return node.type === 'iconLabelNode';
Expand All @@ -25,12 +27,15 @@ export class IconLabelNodeLayoutHandler implements INodeLayoutHandler<IconLabelN
_previousDiagram: Diagram | null,
node: Node<IconLabelNodeData>,
visibleNodes: Node<NodeData, DiagramNodeType>[],
_directChildren: Node<NodeData, DiagramNodeType>[]
_directChildren: Node<NodeData, DiagramNodeType>[],
forceWidth?: number
) {
const nodeIndex = this.findNodeIndex(visibleNodes, node.id);
const labelElement = document.getElementById(`${node.id}-label-${nodeIndex}`);

node.width = labelElement?.getBoundingClientRect().width;
node.width =
forceWidth ??
rectangularNodePadding + (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding;
node.height = labelElement?.getBoundingClientRect().height;
}

Expand Down
Expand Up @@ -30,9 +30,10 @@ export class ImageNodeLayoutHandler implements INodeLayoutHandler<ImageNodeData>
previousDiagram: Diagram | null,
node: Node<ImageNodeData, 'imageNode'>,
_visibleNodes: Node<NodeData, DiagramNodeType>[],
_directChildren: Node<NodeData, DiagramNodeType>[]
_directChildren: Node<NodeData, DiagramNodeType>[],
forceWidth?: number
) {
node.width = defaultWidth;
node.width = forceWidth ?? defaultWidth;
node.height = defaultHeight;

const previousNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === node.id);
Expand Down
Expand Up @@ -31,15 +31,16 @@ export class LayoutEngine implements ILayoutEngine {
public layoutNodes(
previousDiagram: Diagram | null,
visibleNodes: Node<NodeData, DiagramNodeType>[],
nodesToLayout: Node<NodeData, DiagramNodeType>[]
nodesToLayout: Node<NodeData, DiagramNodeType>[],
forceWidth?: number
) {
nodesToLayout.forEach((node) => {
const nodeLayoutHandler: INodeLayoutHandler<NodeData> | undefined = this.nodeLayoutHandlers.find((handler) =>
handler.canHandle(node)
);
if (nodeLayoutHandler) {
const directChildren = visibleNodes.filter((visibleNode) => visibleNode.parentNode === node.id);
nodeLayoutHandler.handle(this, previousDiagram, node, visibleNodes, directChildren);
nodeLayoutHandler.handle(this, previousDiagram, node, visibleNodes, directChildren, forceWidth);

node.style = {
...node.style,
Expand Down
Expand Up @@ -19,7 +19,8 @@ export interface ILayoutEngine {
layoutNodes(
previousDiagram: Diagram | null,
visibleNodes: Node<NodeData, DiagramNodeType>[],
nodesToLayout: Node<NodeData, DiagramNodeType>[]
nodesToLayout: Node<NodeData, DiagramNodeType>[],
forceWidth?: number
);
}

Expand All @@ -30,6 +31,7 @@ export interface INodeLayoutHandler<T extends NodeData> {
previousDiagram: Diagram | null,
node: Node<T>,
visibleNodes: Node<NodeData, DiagramNodeType>[],
directChildren: Node<NodeData, DiagramNodeType>[]
directChildren: Node<NodeData, DiagramNodeType>[],
forceWidth?: number
);
}
Expand Up @@ -17,7 +17,6 @@ import { ListNodeData } from '../node/ListNode.types';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { ILayoutEngine, INodeLayoutHandler } from './LayoutEngine.types';

const rectangularNodePadding = 8;
const defaultWidth = 150;
const defaultHeight = 70;

Expand All @@ -31,75 +30,73 @@ export class ListNodeLayoutHandler implements INodeLayoutHandler<ListNodeData> {
previousDiagram: Diagram | null,
node: Node<ListNodeData, 'listNode'>,
visibleNodes: Node<NodeData, DiagramNodeType>[],
directChildren: Node<NodeData, DiagramNodeType>[]
directChildren: Node<NodeData, DiagramNodeType>[],
forceWidth?: number
) {
const nodeIndex = this.findNodeIndex(visibleNodes, node.id);
const nodeElement = document.getElementById(`${node.id}-listNode-${nodeIndex}`)?.children[0];
const borderWidth = nodeElement ? parseFloat(window.getComputedStyle(nodeElement).borderWidth) : 0;

if (directChildren.length > 0) {
this.handleParentNode(layoutEngine, previousDiagram, node, visibleNodes, directChildren, borderWidth);
this.handleParentNode(layoutEngine, previousDiagram, node, visibleNodes, directChildren, borderWidth, forceWidth);
} else {
this.handleLeafNode(previousDiagram, node, visibleNodes, borderWidth);
this.handleLeafNode(previousDiagram, node, visibleNodes, borderWidth, forceWidth);
}
}
handleLeafNode(
_previousDiagram: Diagram | null,
node: Node<ListNodeData, 'listNode'>,
_visibleNodes: Node<NodeData, DiagramNodeType>[],
_borderWidth: number
visibleNodes: Node<NodeData, DiagramNodeType>[],
borderWidth: number,
forceWidth?: number
) {
node.width = this.getNodeOrMinWidth(undefined);
node.height = this.getNodeOrMinHeight(undefined);
const labelElement = document.getElementById(`${node.id}-label-${this.findNodeIndex(visibleNodes, node.id)}`);

const nodeWidth = (labelElement?.getBoundingClientRect().width ?? 0) + borderWidth * 2;
const nodeHeight = (labelElement?.getBoundingClientRect().height ?? 0) + borderWidth * 2;
node.width = forceWidth ?? this.getNodeOrMinWidth(nodeWidth);
node.height = this.getNodeOrMinHeight(nodeHeight);
}
private handleParentNode(
layoutEngine: ILayoutEngine,
previousDiagram: Diagram | null,
node: Node<ListNodeData, 'listNode'>,
visibleNodes: Node<NodeData, DiagramNodeType>[],
directChildren: Node<NodeData, DiagramNodeType>[],
borderWidth: number
borderWidth: number,
forceWidth?: number
) {
layoutEngine.layoutNodes(previousDiagram, visibleNodes, directChildren);
layoutEngine.layoutNodes(previousDiagram, visibleNodes, directChildren, forceWidth);

const nodeIndex = this.findNodeIndex(visibleNodes, node.id);
const labelElement = document.getElementById(`${node.id}-label-${nodeIndex}`);

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

layoutEngine.layoutNodes(previousDiagram, visibleNodes, 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 = this.getChildrenFootprint(iconLabelNodes);
const childrenAwareNodeWidth = childrenFootprint.x + childrenFootprint.width + rectangularNodePadding;
const labelOnlyWidth =
rectangularNodePadding + (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding;
const nodeWidth = Math.max(childrenAwareNodeWidth, labelOnlyWidth) + borderWidth * 2;
node.width = this.getNodeOrMinWidth(nodeWidth);
node.height = this.getNodeOrMinHeight(
(labelElement?.getBoundingClientRect().height ?? 0) +
rectangularNodePadding +
childrenFootprint.height +
borderWidth * 2
);

if (nodeWidth > childrenAwareNodeWidth) {
// we need to adjust the width of children
iconLabelNodes.forEach((child) => {
child.width = nodeWidth;
child.style = {
...child.style,
width: `${nodeWidth}px`,
};
});
}
const childrenFootprint = this.getChildrenFootprint(directChildren);
const labelOnlyWidth = labelElement?.getBoundingClientRect().width ?? 0;
const nodeWidth = Math.max(childrenFootprint.width, labelOnlyWidth) + borderWidth * 2;
const nodeHeight = (labelElement?.getBoundingClientRect().height ?? 0) + childrenFootprint.height + borderWidth * 2;
node.width = forceWidth ?? this.getNodeOrMinWidth(nodeWidth);
node.height = this.getNodeOrMinHeight(nodeHeight);
}

private findNodeIndex(nodes: Node<NodeData>[], nodeId: string): number {
Expand Down

0 comments on commit f92bda7

Please sign in to comment.