Skip to content

Commit 4df8301

Browse files
Adds support for defining element and relationship styles for light and dark mode (re: #368).
1 parent 1569be3 commit 4df8301

File tree

17 files changed

+267
-35
lines changed

17 files changed

+267
-35
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## v4.2.0 (unreleased)
44

55
- structurizr-dsl: Adds support for `iconPosition` on element styles (options are `Top`, `Bottom`, `Left`).
6+
- structurizr-dsl: Adds support for defining element and relationship styles for light and dark mode.
67

78
## v4.1.0 (28th May 2025)
89

structurizr-core/src/main/java/com/structurizr/view/AbstractStyle.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,28 @@
88

99
public abstract class AbstractStyle implements PropertyHolder {
1010

11+
private ColorScheme colorScheme = null;
12+
1113
private Map<String, String> properties = new HashMap<>();
1214

15+
/**
16+
* Gets the color scheme of this style.
17+
*
18+
* @return a ColorScheme, or null if not specified (i.e. applies to light and dark).
19+
*/
20+
public ColorScheme getColorScheme() {
21+
return colorScheme;
22+
}
23+
24+
/**
25+
* Sets the color scheme of this style.
26+
*
27+
* @param colorScheme a ColorScheme, or null if not specified (i.e. applies to light and dark).
28+
*/
29+
void setColorScheme(ColorScheme colorScheme) {
30+
this.colorScheme = colorScheme;
31+
}
32+
1333
/**
1434
* Gets the collection of name-value property pairs associated with this workspace, as a Map.
1535
*
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.structurizr.view;
2+
3+
/**
4+
* Represents light or dark mode color schemes.
5+
*/
6+
public enum ColorScheme {
7+
8+
Light,
9+
Dark
10+
11+
}

structurizr-core/src/main/java/com/structurizr/view/ElementStyle.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ public final class ElementStyle extends AbstractStyle {
6363
this.tag = tag;
6464
}
6565

66+
ElementStyle(String tag, ColorScheme colorScheme) {
67+
this.tag = tag;
68+
setColorScheme(colorScheme);
69+
}
70+
6671
public ElementStyle(String tag, Integer width, Integer height, String background, String color, Integer fontSize) {
6772
this(tag, width, height, background, color, fontSize, null);
6873
}

structurizr-core/src/main/java/com/structurizr/view/RelationshipStyle.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public final class RelationshipStyle extends AbstractStyle {
5454
this.tag = tag;
5555
}
5656

57+
RelationshipStyle(String tag, ColorScheme colorScheme) {
58+
this.tag = tag;
59+
setColorScheme(colorScheme);
60+
}
61+
5762
public String getTag() {
5863
return tag;
5964
}

structurizr-core/src/main/java/com/structurizr/view/Styles.java

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,24 @@ public void add(ElementStyle elementStyle) {
2929
throw new IllegalArgumentException("A tag must be specified.");
3030
}
3131

32-
if (elements.stream().anyMatch(es -> es.getTag().equals(elementStyle.getTag()))) {
33-
throw new IllegalArgumentException("An element style for the tag \"" + elementStyle.getTag() + "\" already exists.");
32+
if (elements.stream().anyMatch(es -> es.getTag().equals(elementStyle.getTag()) && es.getColorScheme() == elementStyle.getColorScheme())) {
33+
if (elementStyle.getColorScheme() == null) {
34+
throw new IllegalArgumentException("An element style for the tag \"" + elementStyle.getTag() + "\" already exists.");
35+
} else {
36+
throw new IllegalArgumentException("An element style for the tag \"" + elementStyle.getTag() + "\" and color scheme " + elementStyle.getColorScheme() + " already exists.");
37+
}
3438
}
3539

3640
this.elements.add(elementStyle);
3741
}
3842
}
3943

4044
public ElementStyle addElementStyle(String tag) {
41-
ElementStyle elementStyle = new ElementStyle(tag);
45+
return addElementStyle(tag, null);
46+
}
47+
48+
public ElementStyle addElementStyle(String tag, ColorScheme colorScheme) {
49+
ElementStyle elementStyle = new ElementStyle(tag, colorScheme);
4250
add(elementStyle);
4351

4452
return elementStyle;
@@ -68,16 +76,24 @@ public void add(RelationshipStyle relationshipStyle) {
6876
throw new IllegalArgumentException("A tag must be specified.");
6977
}
7078

71-
if (relationships.stream().anyMatch(es -> es.getTag().equals(relationshipStyle.getTag()))) {
72-
throw new IllegalArgumentException("A relationship style for the tag \"" + relationshipStyle.getTag() + "\" already exists.");
79+
if (relationships.stream().anyMatch(rs -> rs.getTag().equals(relationshipStyle.getTag()) && rs.getColorScheme() == relationshipStyle.getColorScheme())) {
80+
if (relationshipStyle.getColorScheme() == null) {
81+
throw new IllegalArgumentException("A relationship style for the tag \"" + relationshipStyle.getTag() + "\" already exists.");
82+
} else {
83+
throw new IllegalArgumentException("A relationship style for the tag \"" + relationshipStyle.getTag() + "\" and color scheme " + relationshipStyle.getColorScheme() + " already exists.");
84+
}
7385
}
7486

7587
this.relationships.add(relationshipStyle);
7688
}
7789
}
7890

7991
public RelationshipStyle addRelationshipStyle(String tag) {
80-
RelationshipStyle relationshipStyle = new RelationshipStyle(tag);
92+
return addRelationshipStyle(tag, null);
93+
}
94+
95+
public RelationshipStyle addRelationshipStyle(String tag, ColorScheme colorScheme) {
96+
RelationshipStyle relationshipStyle = new RelationshipStyle(tag, colorScheme);
8197
add(relationshipStyle);
8298

8399
return relationshipStyle;
@@ -90,11 +106,22 @@ public RelationshipStyle addRelationshipStyle(String tag) {
90106
* @return an ElementStyle instance, or null if no element style has been defined in this workspace
91107
*/
92108
public ElementStyle getElementStyle(String tag) {
109+
return getElementStyle(tag, null);
110+
}
111+
112+
/**
113+
* Gets the element style that has been defined (in this workspace) for the given tag and color scheme.
114+
*
115+
* @param tag the tag (a String)
116+
* @param colorScheme the ColorScheme (can be null)
117+
* @return an ElementStyle instance, or null if no element style has been defined in this workspace
118+
*/
119+
public ElementStyle getElementStyle(String tag, ColorScheme colorScheme) {
93120
if (StringUtils.isNullOrEmpty(tag)) {
94121
throw new IllegalArgumentException("A tag must be specified.");
95122
}
96123

97-
return elements.stream().filter(es -> es.getTag().equals(tag)).findFirst().orElse(null);
124+
return elements.stream().filter(es -> es.getTag().equals(tag) && es.getColorScheme() == colorScheme).findFirst().orElse(null);
98125
}
99126

100127
/**
@@ -141,11 +168,22 @@ public ElementStyle findElementStyle(String tag) {
141168
* @return an RelationshipStyle instance, or null if no relationship style has been defined in this workspace
142169
*/
143170
public RelationshipStyle getRelationshipStyle(String tag) {
171+
return getRelationshipStyle(tag, null);
172+
}
173+
174+
/**
175+
* Gets the relationship style that has been defined (in this workspace) for the given tag and color scheme.
176+
*
177+
* @param tag the tag (a String)
178+
* @param colorScheme the ColorScheme (can be null)
179+
* @return an RelationshipStyle instance, or null if no relationship style has been defined in this workspace
180+
*/
181+
public RelationshipStyle getRelationshipStyle(String tag, ColorScheme colorScheme) {
144182
if (StringUtils.isNullOrEmpty(tag)) {
145183
throw new IllegalArgumentException("A tag must be specified.");
146184
}
147185

148-
return relationships.stream().filter(rs -> rs.getTag().equals(tag)).findFirst().orElse(null);
186+
return relationships.stream().filter(rs -> rs.getTag().equals(tag) && rs.getColorScheme() == colorScheme).findFirst().orElse(null);
149187
}
150188

151189
/**

structurizr-core/src/test/java/com/structurizr/view/StylesTests.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
public class StylesTests extends AbstractWorkspaceTestBase {
1212

13-
private Styles styles = new Styles();
13+
private final Styles styles = new Styles();
1414

1515
@Test
1616
void findElementStyle_ReturnsTheDefaultStyle_WhenPassedNull() {
@@ -263,6 +263,27 @@ void addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlread
263263
}
264264
}
265265

266+
@Test
267+
void addElementStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagAndColorSchemeExistsAlready() {
268+
try {
269+
styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Dark);
270+
styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Dark);
271+
272+
fail();
273+
} catch (IllegalArgumentException iae) {
274+
assertEquals("An element style for the tag \"Software System\" and color scheme Dark already exists.", iae.getMessage());
275+
}
276+
}
277+
278+
@Test
279+
void addElementStyleByTag_WithDifferentColorSchemes() {
280+
styles.addElementStyle(Tags.SOFTWARE_SYSTEM);
281+
styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Dark);
282+
styles.addElementStyle(Tags.SOFTWARE_SYSTEM, ColorScheme.Light);
283+
284+
assertEquals(3, styles.getElements().size());
285+
}
286+
266287
@Test
267288
void addElementStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() {
268289
try {
@@ -311,6 +332,27 @@ void addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagExistsA
311332
}
312333
}
313334

335+
@Test
336+
void addRelationshipStyleByTag_ThrowsAnException_WhenAStyleWithTheSameTagAndColorSchemeExistsAlready() {
337+
try {
338+
styles.addRelationshipStyle(Tags.RELATIONSHIP, ColorScheme.Light);
339+
styles.addRelationshipStyle(Tags.RELATIONSHIP, ColorScheme.Light);
340+
341+
fail();
342+
} catch (IllegalArgumentException iae) {
343+
assertEquals("A relationship style for the tag \"Relationship\" and color scheme Light already exists.", iae.getMessage());
344+
}
345+
}
346+
347+
@Test
348+
void addRelationshipStyleByTag_WithDifferentColorSchemes() {
349+
styles.addRelationshipStyle(Tags.RELATIONSHIP);
350+
styles.addRelationshipStyle(Tags.RELATIONSHIP, ColorScheme.Dark);
351+
styles.addRelationshipStyle(Tags.RELATIONSHIP, ColorScheme.Light);
352+
353+
assertEquals(3, styles.getRelationships().size());
354+
}
355+
314356
@Test
315357
void addRelationshipStyle_ThrowsAnException_WhenAStyleWithTheSameTagExistsAlready() {
316358
try {

structurizr-dsl/src/main/java/com/structurizr/dsl/ElementStyleParser.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
import com.structurizr.Workspace;
44
import com.structurizr.util.ImageUtils;
55
import com.structurizr.util.StringUtils;
6-
import com.structurizr.view.Border;
7-
import com.structurizr.view.ElementStyle;
8-
import com.structurizr.view.IconPosition;
9-
import com.structurizr.view.Shape;
6+
import com.structurizr.view.*;
107

118
import java.io.File;
129
import java.io.IOException;
@@ -17,7 +14,7 @@ final class ElementStyleParser extends AbstractParser {
1714

1815
private static final int FIRST_PROPERTY_INDEX = 1;
1916

20-
ElementStyle parseElementStyle(DslContext context, Tokens tokens) {
17+
ElementStyle parseElementStyle(StylesDslContext context, Tokens tokens) {
2118
if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) {
2219
throw new RuntimeException("Too many tokens, expected: element <tag> {");
2320
} else if (tokens.includes(FIRST_PROPERTY_INDEX)) {
@@ -28,9 +25,9 @@ ElementStyle parseElementStyle(DslContext context, Tokens tokens) {
2825
}
2926

3027
Workspace workspace = context.getWorkspace();
31-
ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().getElementStyle(tag);
28+
ElementStyle elementStyle = workspace.getViews().getConfiguration().getStyles().getElementStyle(tag, context.getColorScheme());
3229
if (elementStyle == null) {
33-
elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle(tag);
30+
elementStyle = workspace.getViews().getConfiguration().getStyles().addElementStyle(tag, context.getColorScheme());
3431
}
3532

3633
return elementStyle;

structurizr-dsl/src/main/java/com/structurizr/dsl/RelationshipStyleParser.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
import com.structurizr.Workspace;
44
import com.structurizr.util.StringUtils;
5-
import com.structurizr.view.LineStyle;
6-
import com.structurizr.view.RelationshipStyle;
7-
import com.structurizr.view.Routing;
5+
import com.structurizr.view.*;
86

97
import java.util.HashMap;
108
import java.util.Map;
@@ -13,7 +11,7 @@ final class RelationshipStyleParser extends AbstractParser {
1311

1412
private static final int FIRST_PROPERTY_INDEX = 1;
1513

16-
RelationshipStyle parseRelationshipStyle(DslContext context, Tokens tokens) {
14+
RelationshipStyle parseRelationshipStyle(StylesDslContext context, Tokens tokens) {
1715
if (tokens.hasMoreThan(FIRST_PROPERTY_INDEX)) {
1816
throw new RuntimeException("Too many tokens, expected: relationship <tag> {");
1917
}
@@ -26,9 +24,9 @@ RelationshipStyle parseRelationshipStyle(DslContext context, Tokens tokens) {
2624
}
2725

2826
Workspace workspace = context.getWorkspace();
29-
RelationshipStyle relationshipStyle = workspace.getViews().getConfiguration().getStyles().getRelationshipStyle(tag);
27+
RelationshipStyle relationshipStyle = workspace.getViews().getConfiguration().getStyles().getRelationshipStyle(tag, context.getColorScheme());
3028
if (relationshipStyle == null) {
31-
relationshipStyle = workspace.getViews().getConfiguration().getStyles().addRelationshipStyle(tag);
29+
relationshipStyle = workspace.getViews().getConfiguration().getStyles().addRelationshipStyle(tag, context.getColorScheme());
3230
}
3331

3432
return relationshipStyle;

structurizr-dsl/src/main/java/com/structurizr/dsl/StructurizrDslParser.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,8 +791,14 @@ void parse(List<String> lines, File dslFile, boolean fragment, boolean includeIn
791791
} else if (STYLES_TOKEN.equalsIgnoreCase(firstToken) && inContext(ViewsDslContext.class)) {
792792
startContext(new StylesDslContext());
793793

794+
} else if (LIGHT_COLOR_SCHEME_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) {
795+
startContext(new StylesDslContext(ColorScheme.Light));
796+
797+
} else if (DARK_COLOR_SCHEME_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) {
798+
startContext(new StylesDslContext(ColorScheme.Dark));
799+
794800
} else if (ELEMENT_STYLE_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) {
795-
ElementStyle elementStyle = new ElementStyleParser().parseElementStyle(getContext(), tokens.withoutContextStartToken());
801+
ElementStyle elementStyle = new ElementStyleParser().parseElementStyle(getContext(StylesDslContext.class), tokens.withoutContextStartToken());
796802
startContext(new ElementStyleDslContext(elementStyle, dslFile));
797803

798804
} else if (ELEMENT_STYLE_BACKGROUND_TOKEN.equalsIgnoreCase(firstToken) && inContext(ElementStyleDslContext.class)) {
@@ -838,7 +844,7 @@ void parse(List<String> lines, File dslFile, boolean fragment, boolean includeIn
838844
new ElementStyleParser().parseIconPosition(getContext(ElementStyleDslContext.class), tokens);
839845

840846
} else if (RELATIONSHIP_STYLE_TOKEN.equalsIgnoreCase(firstToken) && inContext(StylesDslContext.class)) {
841-
RelationshipStyle relationshipStyle = new RelationshipStyleParser().parseRelationshipStyle(getContext(), tokens.withoutContextStartToken());
847+
RelationshipStyle relationshipStyle = new RelationshipStyleParser().parseRelationshipStyle(getContext(StylesDslContext.class), tokens.withoutContextStartToken());
842848
startContext(new RelationshipStyleDslContext(relationshipStyle));
843849

844850
} else if (RELATIONSHIP_STYLE_THICKNESS_TOKEN.equalsIgnoreCase(firstToken) && inContext(RelationshipStyleDslContext.class)) {

0 commit comments

Comments
 (0)