Back to top

Client-side Layouting

We distinguish between micro and macro layout. Usually, the server is responsible for the macro layout, that is the arrangement of the main model elements (i.e. nodes and edges). This layout is defined already in the graphical model by means of coordinates. In turn, the client is responsible for the micro layout, that is the positioning and size computation of elements within a container element such as nodes. The client side (i.e. micro) layout can be configured in the graphical model, but will be applied on the client during the rendering phase.

Layout Container

Graphical elements that support client-side layouting of contained elements offer a layout property which defines the type of layouter that should be used. In addition, the behavior of the layouter can be configured with a set of layout options.

For an example let’s have a look at the following GNode:

Java GLSP Server
new GNodeBuilder()
   .layout(GConstants.Layout.VBOX)
   .layoutOptions(new GLayoutOptions()
      .hAlign(GConstants.HAlign.Center))
   .add(new GLabelBuilder()
      .text("label1")
      .build())
   .add(new GLabelBuilder()
      .text("label2")
      .build())
   .build();
Node GLSP Server
GNode.builder()
  .layout("vbox")
  .addLayoutOption("hAlign", "center")
  .add(GLabel.builder().text("label1").build())
  .add(GLabel.builder().text("label2").build())
  .build();

This node contains two label elements that should be layouted vertically (from top to bottom) using the vbox layouter. In addition, we specify options for that layouter by defining that the children should be centered horizontally.

Layout Options

To adapt the chosen Layout (for more details please see below), the default Layout options AbstractLayoutOptions allow to configure:

  • resizeContainer: boolean
    Indicates, if the container is resizable depending on its children (e.g. true for hbox layout)
  • paddingTop: number
    Sets the height of the padding area on the top of an element to be positioned in pixel.
  • paddingBottom: number
    Sets the height of the padding area on the bottom of an element to be positioned in pixel.
  • paddingLeft: number
    Sets the height of the padding area to the left of an element to be positioned in pixel.
  • paddingRight: number
    Sets the height of the padding area to the right of an element to be positioned in pixel.
  • paddingFactor: number
    Defines the multiplication factor for an element’s size, which is then added as padding space around this element. E.g. For the stack layout 1 does not add additional padding to the element, 2 doubles the padding area around the element by its size.
  • minWidth: number
    Defines the minimum width of an element to be positioned.
  • minHeight: number
    Defines the minimum height of an element to be positioned.
Usage Java GLSP Server

To use the Layout options on the GLSP Java Server, there are several utility classes for option keys and values, for example:

  • Edge label placement GConstants.EdgeSide: LEFT | RIGHT | TOP | BOTTOM | ON
  • Horizontal alignment GConstants.HAlign: LEFT | CENTER | RIGHT

For more details, please see GLayoutOptions and GConstants.

Usage Node GLSP Server

To use the Layout options on the GLSP Node Server, the support via utility classes for option keys and values is currently very limited, e.g. only the EdgeSide type is available. Therefore please use the dedicated string values in the meantime, e.g. "hAlign" "center" and so on.


Default GLSP Layouters

In general, layouters can be applied to elements that are compartments, in order to layout the containers based on the sizes of their children.

There are four built-in layout types that can be used: hbox, vbox, freeform and stack.

hbox Layout

The HBoxLayouterExt layouts children of a container in a horizontal (left to right) direction.

This layouter provides additional layout options via HBoxLayoutOptionsExt:

  • hGap: number
    Defines a horizontal gap between elements in the same container in pixel.
  • vAlign: VAlignment = ‘top’ | ‘center’ | ‘bottom’
    Defines the vertical alignment of the element to be positioned.
  • hGrab: boolean
    Indicates whether the remaining horizontal size can be grabbed by its children.
  • vGrab: boolean
    Indicates whether the remaining vertical size can be grabbed by its children.
  • prefWidth: number | null
    Defines the preferred width of the container element, which will be assigned, if there is no manually specified width and the sizes of its children aren’t requiring a larger width to fit into the container.
  • prefHeight: number | null
    Defines the preferred height of the container element, which will be assigned, if there is no manually specified height and the sizes of its children aren’t requiring a larger height to fit into the container.

According to the layout options, the children of the container are layouted and as concluding step, the final bounds of the container are computed based on the sum of this children bounds.

hbox Layout Example

This example creates a compartment of the default type DefaultTypes.COMPARTMENT using the hbox layout. It adds two children, one icon compartment (with custom type "icon") and one label. The layout options define a horizontal gap of 15 pixel between the children of the compartment.

Java GLSP Server
   new GCompartmentBuilder()
         .type(DefaultTypes.COMPARTMENT)
         .layout(GConstants.Layout.HBOX)
         .layoutOptions(new GLayoutOptions().hGap(15))
         .add(new GCompartmentBuilder()
            .type("icon")
            .build())
         .add(new GLabelBuilder()
            .text("label")
            .build())
         .build();
   .build();
Node GLSP Server
GCompartment.builder()
  .type("comp")
  .layout("hbox")
  .addLayoutOption("hGap", 15)
  .add(GCompartment.builder().type("icon").build())
  .add(GLabel.builder().text("label").build())
  .build();

On the client side, we need to configure (besides the default elements SCompartment and SLabel) a custom Icon element and a IconView like this: configureModelElement(context, 'icon', Icon, IconView). The Icon element definition and the IconView definition are taken from the workflow example.

The resulting element with the obvious horizontal gap between the child elements is shown in the following image:

example-hbox-layout

Using no `hGap` layout option (left) or `hGap` layout option with 15 pixel (right)



vbox Layout

The VBoxLayouterExt layouts children of a container in a vertical (top to bottom) direction.

This layouter provides additional layout options via VBoxLayoutOptionsExt:

  • vGap: number
    Defines a vertical gap between elements in the same container in pixel.
  • hAlign: HAlignment = ‘left’ | ‘center’ | ‘right’
    Defines the horizontal alignment of the element to be positioned.
  • hGrab: boolean
    Indicates whether the remaining horizontal size can be grabbed by its children.
  • vGrab: boolean
    Indicates whether the remaining vertical size can be grabbed by its children.
  • prefWidth: number | null
    Defines the preferred width of the container element, which will be assigned, if there is no manually specified width and the sizes of its children aren’t requiring a larger width to fit into the container.
  • prefHeight: number | null
    Defines the preferred height of the container element, which will be assigned, if there is no manually specified height and the sizes of its children aren’t requiring a larger height to fit into the container.

According to the layout options, the children of the container are layouted and as concluding step, the final bounds of the container are computed based on the sum of this children bounds.

vbox Layout Example

This example creates a compartment of the default type DefaultTypes.COMPARTMENT using the vbox layout. It adds two children labels, which are positioned vertically from top to bottom and are centered horizontally.

Java GLSP Server
   new GCompartmentBuilder()
         .type(DefaultTypes.COMPARTMENT)
         .layout(GConstants.Layout.VBOX)
         .layoutOptions(new GLayoutOptions().hAlign(GConstants.HAlign.CENTER))
         .add(new GCompartmentBuilder()
            .type("icon")
            .build())
         .add(new GLabelBuilder()
            .text("Label")
            .build())
         .build();
   .build();
Node GLSP Server
GCompartment.builder()
  .type("comp")
  .layout("vbox")
  .addLayoutOption("hAlign", "center")
  .add(GCompartment.builder().type("icon").build())
  .add(GLabel.builder().text("Label").build())
  .build();

On the client side, we use again the Icon element definition and IconView, please see section hbox Layout Example.

The resulting element positions its children vertically from top to bottom and aligns each element horizontally, this is shown in the following image:

example-vbox-layout

Center children horizontally (left image) or align children to the left (right image)



freeform Layout

The FreeFormLayouter positions the children of a container according to their explicit, parent-relative x/y coordinates inside the parent container. This layouter uses the default Layout Options and provides suitable default values (e.g. resizeContainer: true).

Again here, the children of the container are positioned according to their explicit positions inside the container and as concluding step, the final bounds of the container are computed based on the required bounds of its children.

freeform Layout Example

This example creates a compartment of the custom type "comp:structure" using the freeform layout. It adds one child node, at the relative position (75, 35) of the parent container. The parent container defines its preferred size of 250 x 125.

Java GLSP Server
  GDimension containerPrefSize = GraphUtil.dimension(250 /*width*/, 125 /*height*/);
  GPoint childPosition = GraphUtil.point(75 /*x*/, 35 /*y*/);

  Map<String, Object> layoutOptions = new HashMap<>();
  layoutOptions.put(H_GRAB, true);
  layoutOptions.put(V_GRAB, true);
  layoutOptions.put(GLayoutOptions.KEY_PREF_WIDTH, containerPrefSize.getWidth());
  layoutOptions.put(GLayoutOptions.KEY_PREF_HEIGHT, containerPrefSize.getHeight());
  new GCompartmentBuilder()
    .type("comp:structure")
    .layout(GConstants.Layout.FREEFORM)
    .layoutOptions(layoutOptions)
    .add(
      new GNodeBuilder(DefaultTypes.NODE)
        .position(childPosition)
        .build()
    )
    .build();
Node GLSP Server
const containerPrefSize = { width: 250, height: 125 };
const childPosition = { x: 75, y: 35 };

const layoutOptions = {
  ["hGrab"]: true,
  ["vGrab"]: true,
  ["prefWidth"]: containerPrefSize.width,
  ["prefHeight"]: containerPrefSize.height,
};
GCompartment.builder()
  .type("comp:structure")
  .layout("freeform")
  .addLayoutOptions(layoutOptions)
  .add(GNode.builder().type("node").position(childPosition).build())
  .build();

On the client side, configure the "comp:structure" compartment as configureModelElement(context, 'struct', SCompartment, StructureCompartmentView).

The resulting compartment element positions its child at the desired position. The compartment defines its preferred size, which is used if the children do not enlarge the container. If the preferred size is omitted, the container’s size depends on its children.

freeform-layout-example

Container with preferred size (left image) or without preferred size (right image)



Default Sprotty Layouters

It is also possible to use default sprotty layouts, like for example the stack Layout as shown below in the example. Here it is necessary to use the sprotty specific Layouter which is available via sprotty’s boundsModule, which is also part of the GLSP client container’s default modules. However, we recommend using GLSP layouts.

stack Layout

The StackLayouter positions the children by stacking them on top of each other considering the given alignment.

This layouter provides additional layout options via StackLayoutOptions:

  • vAlign: VAlignment = ‘top’ | ‘center’ | ‘bottom’
    Defines the vertical alignment of the element to be positioned.
  • hAlign: HAlignment = ‘left’ | ‘center’ | ‘right’
    Defines the horizontal alignment of the element to be positioned.

The children of the container are positioned according to their order and the defined alignment. Based on that, the maximum height and width are computed and are used as bounds for the container.

`stack` Layout Example

This example creates a compartment of the custom type "comp" using the stack layout. It adds two child nodes - a circle a node and a text label - that are stacked on top of each other.

Java GLSP Server
  Map<String, Object> layoutOptions = new HashMap<>();
  layoutOptions.put(GLayoutOptions.KEY_H_ALIGN, GConstants.HAlign.CENTER);
  layoutOptions.put(GLayoutOptions.KEY_RESIZE_CONTAINER, false);
  new GCompartmentBuilder()
    .type(DefaultTypes.COMPARTMENT)
    .layout(GConstants.Layout.STACK)
    .layoutOptions(layoutOptions)
    .size(GraphUtil.dimension(35, 35))
    .add(
      new GNodeBuilder("circle")
        .size(GraphUtil.dimension(35, 35))
        .build()
    )
    .add(
      new GLabelBuilder()
        .text("A")
        .addCssClass("circle-letter")
        .build()
    )
    .build();
Node GLSP Server
const layoutOptions = {
  ["hAlign"]: "center",
  ["resizeContainer"]: false,
};
GCompartment.builder()
  .type("comp")
  .layout("struct")
  .addLayoutOptions(layoutOptions)
  .add(GNode.builder().type("circle").size(35, 35).build())
  .add(GLabel.builder().text("A").addCssClass("circle-letter").build())
  .build();

On the client side, please make sure that sprotty’s boundsModule is registered.

The "circle" node element has to be configured as configureModelElement(context, 'circle', CircularNode, CircularNodeView).

To style the letter label, we add this simple CSS rule as well:

.circle-letter {
  fill: lightsteelblue;
}

The resulting compartment positions its children on top of each other and centers the label horizontally.

freeform-layout-example

Container that stacks its children and positions the label to the left (1), in the center (2) and to the right (3)



Custom Layouter

Additional custom layouters can be contributed by creating a layouter class that extends the AbstractLayouter and optionally provide custom options that extend the AbstractLayoutOptions.

In the following example we show a simple custom layouter that extends the VBoxLayouterExt and provides a custom layout option, which increases the widths and heights of all children by a specified value, the default value is 50 pixel.

GLSP Client
export interface MyCustomLayoutOptions extends VBoxLayoutOptionsExt {
  enlargeSizeBy: number;
}

@injectable()
export class MyCustomLayouter extends VBoxLayouterExt {
  static override KIND = "myCustomLayout";

  protected override getChildrenSize(
    container: SParentElement & LayoutContainer,
    containerOptions: MyCustomLayoutOptions,
    layouter: StatefulLayouter
  ): Dimension {
    const result = super.getChildrenSize(container, containerOptions, layouter);
    return {
      width: result.width + containerOptions.enlargeSizeBy,
      height: result.height + containerOptions.enlargeSizeBy,
    };
  }

  protected override getDefaultLayoutOptions(): MyCustomLayoutOptions {
    return {
      enlargeSizeBy: 50,
      ...super.getDefaultLayoutOptions(),
    };
  }

  protected override spread(
    a: MyCustomLayoutOptions,
    b: MyCustomLayoutOptions
  ): MyCustomLayoutOptions {
    return { ...a, ...b };
  }
}

Then the new custom layout needs to bound in the diagram module (di.config.ts):

GLSP Client
const myDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => {
   rebind(VBoxLayouter).to(MyCustomLayouter);
}

Custom layouter, which don’t overwrite existing ones, should extend the AbstractLayout and can be registered for a certain layouter key, e.g. MyCustomLayouter.KIND, that is used in the layout property of compartments as follows:

GLSP Client
const myDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => {
   configureLayout({ bind, isBound }, MyCustomLayouter.KIND, MyCustomLayouter);
}

With that, the custom layouter is ready to be used by compartments in the graphical model:

Java GLSP Server
   new GCompartmentBuilder()
         .type(DefaultTypes.COMPARTMENT)
         .layout("myCustomLayout")
         .layoutOptions(Map.of("enlargeSizeBy", 15))
         .add(new GLabelBuilder()
            .text("label")
            .build())
         .build();
   .build();
Node GLSP Server
GCompartment.builder()
  .builder()
  .layout("myCustomLayout")
  .addLayoutOption("enlargeSizeBy", 15)
  .add(GLabel.builder().text("label").build())
  .build();

Edge Layout

Graphical elements that are typically rendered in combination with an edge such as labels have an edgeLayout property. This can be used to describe how the element should be aligned with the edge.

For example let’s have a look at the following GLabel:

Java GLSP Server
new GLabelBuilder()
   .edgePlacement(new GEdgePlacementBuilder()
      .side(GConstants.EdgeSide.TOP)
      .position(0.5)
      .build())
   .add(new GLabelBuilder().text("MyLabel").build())
   .build();
Node GLSP Server
GLabel.builder()
  .edgePlacement({ side: "top", position: 0.5, rotate: false, offset: 0 })
  .add(new GLabelBuilder(GLabel).text("MyLabel").build())
  .build();

The label above specifies the property edgePlacement. If this label is added as a child of an edge, it will be placed above the edge. The position is defined with 0.5 (i.e. 50 percent of the edge length) which means the label should be placed in the center of the edge. After the edge routes have been rendered the client will query all active edge placements and adjust the label position accordingly.