-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathShapeContainerBuilder.java
More file actions
379 lines (342 loc) · 12.5 KB
/
ShapeContainerBuilder.java
File metadata and controls
379 lines (342 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
package com.demcha.compose.document.dsl;
import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.LayerAlign;
import com.demcha.compose.document.node.LayerStackNode;
import com.demcha.compose.document.node.ShapeContainerNode;
import com.demcha.compose.document.style.ClipPolicy;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.style.DocumentTransform;
import com.demcha.compose.document.style.ShapeOutline;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Builder for {@link ShapeContainerNode}.
*
* <p>Reads as: <em>"container is a [shape], inside it I'm composing layers"</em>.
* The outline is mandatory — call {@link #rectangle(double, double)},
* {@link #roundedRect(double, double, double)}, {@link #ellipse(double, double)},
* or {@link #circle(double)} before {@link #build()}. Layers are appended in
* source order (first behind, last in front) and each layer carries one of the
* nine {@link LayerAlign} anchors plus optional on-screen offset.</p>
*
* @author Artem Demchyshyn
*/
public final class ShapeContainerBuilder implements Transformable<ShapeContainerBuilder> {
private String name = "";
private ShapeOutline outline;
private final List<LayerStackNode.Layer> layers = new ArrayList<>();
// Default to CLIP_PATH per ADR §Decision — see ShapeContainerNode for
// the rationale.
private ClipPolicy clipPolicy = ClipPolicy.CLIP_PATH;
private DocumentColor fillColor;
private DocumentStroke stroke;
private DocumentInsets padding = DocumentInsets.zero();
private DocumentInsets margin = DocumentInsets.zero();
private DocumentTransform transform = DocumentTransform.NONE;
/**
* Creates a shape-container builder with no outline configured yet.
*/
public ShapeContainerBuilder() {
}
/**
* Sets the semantic node name.
*
* @param name name used in snapshots and layout graph paths
* @return this builder
*/
public ShapeContainerBuilder name(String name) {
this.name = name == null ? "" : name;
return this;
}
/**
* Sets a plain rectangle outline.
*
* @param width outline width in points
* @param height outline height in points
* @return this builder
*/
public ShapeContainerBuilder rectangle(double width, double height) {
this.outline = new ShapeOutline.Rectangle(width, height);
return this;
}
/**
* Sets a rounded-rectangle outline.
*
* @param width outline width in points
* @param height outline height in points
* @param cornerRadius corner radius in points
* @return this builder
*/
public ShapeContainerBuilder roundedRect(double width, double height, double cornerRadius) {
this.outline = new ShapeOutline.RoundedRectangle(width, height, cornerRadius);
return this;
}
/**
* Sets an ellipse outline.
*
* @param width outline width in points
* @param height outline height in points
* @return this builder
*/
public ShapeContainerBuilder ellipse(double width, double height) {
this.outline = new ShapeOutline.Ellipse(width, height);
return this;
}
/**
* Sets a circular outline (ellipse with equal width and height).
*
* @param diameter diameter in points
* @return this builder
*/
public ShapeContainerBuilder circle(double diameter) {
this.outline = ShapeOutline.circle(diameter);
return this;
}
/**
* Replaces the outline with a pre-built {@link ShapeOutline} value.
*
* @param outline outline value
* @return this builder
*/
public ShapeContainerBuilder outline(ShapeOutline outline) {
this.outline = Objects.requireNonNull(outline, "outline");
return this;
}
/**
* Sets the clipping policy applied to child layers.
*
* @param clipPolicy clip policy
* @return this builder
*/
public ShapeContainerBuilder clipPolicy(ClipPolicy clipPolicy) {
this.clipPolicy = clipPolicy == null ? ClipPolicy.CLIP_PATH : clipPolicy;
return this;
}
/**
* Sets outline fill colour with a public canonical value.
*
* @param fillColor fill colour, or {@code null} for no fill
* @return this builder
*/
public ShapeContainerBuilder fillColor(DocumentColor fillColor) {
this.fillColor = fillColor;
return this;
}
/**
* Sets outline fill colour from an AWT colour value.
*
* @param fillColor fill colour, or {@code null} for no fill
* @return this builder
*/
public ShapeContainerBuilder fillColor(Color fillColor) {
this.fillColor = fillColor == null ? null : DocumentColor.of(fillColor);
return this;
}
/**
* Sets outline stroke.
*
* @param stroke stroke descriptor, or {@code null} for no stroke
* @return this builder
*/
public ShapeContainerBuilder stroke(DocumentStroke stroke) {
this.stroke = stroke;
return this;
}
/**
* Sets uniform inner padding around all layers.
*
* @param padding padding in points
* @return this builder
*/
public ShapeContainerBuilder padding(double padding) {
return padding(DocumentInsets.of(padding));
}
/**
* Sets inner padding around all layers.
*
* @param padding padding insets
* @return this builder
*/
public ShapeContainerBuilder padding(DocumentInsets padding) {
this.padding = padding == null ? DocumentInsets.zero() : padding;
return this;
}
/**
* Sets uniform outer margin around the container.
*
* @param margin margin in points
* @return this builder
*/
public ShapeContainerBuilder margin(double margin) {
return margin(DocumentInsets.of(margin));
}
/**
* Sets outer margin around the container.
*
* @param margin margin insets
* @return this builder
*/
public ShapeContainerBuilder margin(DocumentInsets margin) {
this.margin = margin == null ? DocumentInsets.zero() : margin;
return this;
}
/**
* Sets the render-time affine transform (rotation around the
* placement centre and/or scaling). The {@link Transformable#rotate(double)},
* {@link Transformable#scale(double)}, and
* {@link Transformable#scale(double, double)} shortcuts delegate
* through this setter.
*
* @param transform new transform; {@code null} resets to
* {@link DocumentTransform#NONE}
* @return this builder
*/
@Override
public ShapeContainerBuilder transform(DocumentTransform transform) {
this.transform = transform == null ? DocumentTransform.NONE : transform;
return this;
}
@Override
public DocumentTransform currentTransform() {
return transform;
}
/**
* Appends a layer with explicit alignment.
*
* @param node child node
* @param align anchor inside the inner box
* @return this builder
*/
public ShapeContainerBuilder layer(DocumentNode node, LayerAlign align) {
layers.add(new LayerStackNode.Layer(Objects.requireNonNull(node, "node"), align));
return this;
}
/**
* Appends a layer with explicit alignment and z-index. Higher
* {@code zIndex} renders on top of lower {@code zIndex} regardless
* of source order; the default is {@code 0}.
*
* @param node child node
* @param align anchor inside the inner box
* @param zIndex render-order key
* @return this builder
*/
public ShapeContainerBuilder layer(DocumentNode node, LayerAlign align, int zIndex) {
layers.add(new LayerStackNode.Layer(
Objects.requireNonNull(node, "node"), align, 0.0, 0.0, zIndex));
return this;
}
/**
* Appends a top-left aligned layer.
*
* @param node child node
* @return this builder
*/
public ShapeContainerBuilder layer(DocumentNode node) {
return layer(node, LayerAlign.TOP_LEFT);
}
/**
* Appends a layer anchored to {@code align} and shifted by an on-screen
* offset (positive {@code offsetX} = right, positive {@code offsetY} = down).
*
* @param node child node
* @param offsetX horizontal offset in points
* @param offsetY vertical offset in points
* @param align anchor inside the inner box
* @return this builder
*/
public ShapeContainerBuilder position(DocumentNode node,
double offsetX,
double offsetY,
LayerAlign align) {
layers.add(new LayerStackNode.Layer(
Objects.requireNonNull(node, "node"), align, offsetX, offsetY));
return this;
}
/**
* Appends a layer anchored to {@code align}, shifted by an on-screen
* offset, and assigned an explicit z-index.
*
* @param node child node
* @param offsetX horizontal offset in points
* @param offsetY vertical offset in points
* @param align anchor inside the inner box
* @param zIndex render-order key
* @return this builder
*/
public ShapeContainerBuilder position(DocumentNode node,
double offsetX,
double offsetY,
LayerAlign align,
int zIndex) {
layers.add(new LayerStackNode.Layer(
Objects.requireNonNull(node, "node"), align, offsetX, offsetY, zIndex));
return this;
}
/**
* Appends a back layer (top-left aligned, drawn first / behind).
*
* @param node child node
* @return this builder
*/
public ShapeContainerBuilder back(DocumentNode node) {
return layer(node, LayerAlign.TOP_LEFT);
}
// 9-point alignment shortcuts mirror LayerStackBuilder so the two builders
// read the same way. Keeping the surface aligned helps autocomplete:
// "I started with addCircle, the same vocabulary works."
/** Anchors a layer to the top-left corner. */
public ShapeContainerBuilder topLeft(DocumentNode node) {
return layer(node, LayerAlign.TOP_LEFT);
}
/** Anchors a layer to the top edge, centred horizontally. */
public ShapeContainerBuilder topCenter(DocumentNode node) {
return layer(node, LayerAlign.TOP_CENTER);
}
/** Anchors a layer to the top-right corner. */
public ShapeContainerBuilder topRight(DocumentNode node) {
return layer(node, LayerAlign.TOP_RIGHT);
}
/** Anchors a layer to the left edge, centred vertically. */
public ShapeContainerBuilder centerLeft(DocumentNode node) {
return layer(node, LayerAlign.CENTER_LEFT);
}
/** Centred layer (the typical foreground content above an outline fill). */
public ShapeContainerBuilder center(DocumentNode node) {
return layer(node, LayerAlign.CENTER);
}
/** Anchors a layer to the right edge, centred vertically. */
public ShapeContainerBuilder centerRight(DocumentNode node) {
return layer(node, LayerAlign.CENTER_RIGHT);
}
/** Anchors a layer to the bottom-left corner. */
public ShapeContainerBuilder bottomLeft(DocumentNode node) {
return layer(node, LayerAlign.BOTTOM_LEFT);
}
/** Anchors a layer to the bottom edge, centred horizontally. */
public ShapeContainerBuilder bottomCenter(DocumentNode node) {
return layer(node, LayerAlign.BOTTOM_CENTER);
}
/** Anchors a layer to the bottom-right corner. */
public ShapeContainerBuilder bottomRight(DocumentNode node) {
return layer(node, LayerAlign.BOTTOM_RIGHT);
}
/**
* Builds the immutable {@link ShapeContainerNode}.
*
* @return the configured container node
* @throws IllegalStateException if no outline was configured
*/
public ShapeContainerNode build() {
if (outline == null) {
throw new IllegalStateException(
"ShapeContainerBuilder '" + name + "' requires an outline; "
+ "call rectangle/roundedRect/ellipse/circle before build().");
}
return new ShapeContainerNode(name, outline, layers, clipPolicy, fillColor, stroke, padding, margin, transform);
}
}