1 module fluid.border;
2
3 import fluid.style;
4 import fluid.backend;
5
6
7 @safe:
8
9
10 /// Interface for borders
11 interface FluidBorder {
12
13 /// Apply the border, drawing it in the given box.
14 void apply(FluidBackend backend, Rectangle borderBox, float[4] size) const;
15
16 /// Borders have to be comparable in a memory-safe manner.
17 bool opEquals(const Object object) @safe const;
18
19 /// Get the rectangle for the given side of the border.
20 final Rectangle sideRect(Rectangle source, float[4] size, Style.Side side) const {
21
22 final switch (side) {
23
24 // Left side
25 case Style.Side.left:
26 return Rectangle(
27 source.x,
28 source.y + size.sideTop,
29 size.sideLeft,
30 source.height - size.sideTop - size.sideBottom,
31 );
32
33 // Right side
34 case Style.Side.right:
35 return Rectangle(
36 source.x + source.width - size.sideRight,
37 source.y + size.sideTop,
38 size.sideRight,
39 source.height - size.sideTop - size.sideBottom,
40 );
41
42 // Top side
43 case Style.Side.top:
44 return Rectangle(
45 source.x + size.sideLeft,
46 source.y,
47 source.width - size.sideLeft - size.sideRight,
48 size.sideTop
49 );
50
51 // Bottom side
52 case Style.Side.bottom:
53 return Rectangle(
54 source.x + size.sideLeft,
55 source.y + source.height - size.sideBottom,
56 source.width - size.sideLeft - size.sideRight,
57 size.sideBottom
58 );
59
60 }
61
62 }
63
64 /// Get square for corner next counter-clockwise to the given side.
65 /// Note: returned rectangles may have negative size; rect start position will always point to the corner itself.
66 final Rectangle cornerRect(Rectangle source, float[4] size, Style.Side side) const {
67
68 final switch (side) {
69
70 case Style.Side.left:
71 return Rectangle(
72 source.x,
73 source.y + source.height,
74 size.sideLeft,
75 -cast(float) size.sideBottom,
76 );
77
78 case Style.Side.right:
79 return Rectangle(
80 source.x + source.width,
81 source.y,
82 -cast(float) size.sideRight,
83 size.sideTop,
84 );
85
86 case Style.Side.top:
87 return Rectangle(
88 source.x,
89 source.y,
90 size.sideLeft,
91 size.sideTop,
92 );
93
94 case Style.Side.bottom:
95 return Rectangle(
96 source.x + source.width,
97 source.y + source.height,
98 -cast(float) size.sideRight,
99 -cast(float) size.sideBottom,
100 );
101
102 }
103
104 }
105
106 }
107
108
109
110 ColorBorder colorBorder(Color color) {
111
112 return colorBorder([color]);
113
114 }
115
116 ColorBorder colorBorder(size_t n)(Color[n] color) {
117
118 auto result = new ColorBorder;
119 result.color = normalizeSideArray!Color(color);
120 return result;
121
122 }
123
124 class ColorBorder : FluidBorder {
125
126 Color[4] color;
127
128 void apply(FluidBackend io, Rectangle borderBox, float[4] size) const @trusted {
129
130 // For each side
131 foreach (sideIndex; 0..4) {
132
133 const side = cast(Style.Side) sideIndex;
134 const nextSide = cast(Style.Side) ((sideIndex + 1) % 4);
135
136 // Draw all the fragments
137 io.drawRectangle(sideRect(borderBox, size, side), color[side]);
138
139 // Draw triangles in the corner
140 foreach (shift; 0..2) {
141
142 // Get the corner
143 const cornerSide = shiftSide(side, shift);
144
145 // Get corner parameters
146 const corner = cornerRect(borderBox, size, cornerSide);
147 const cornerStart = Vector2(corner.x, corner.y);
148 const cornerSize = Vector2(corner.w, corner.h);
149 const cornerEnd = side < 2
150 ? Vector2(0, corner.h)
151 : Vector2(corner.w, 0);
152
153 // Draw the first triangle
154 if (!shift)
155 io.drawTriangle(
156 cornerStart,
157 cornerStart + cornerSize,
158 cornerStart + cornerEnd,
159 color[side],
160 );
161
162 // Draw the second one
163 else
164 io.drawTriangle(
165 cornerStart,
166 cornerStart + cornerEnd,
167 cornerStart + cornerSize,
168 color[side],
169 );
170
171 }
172
173 }
174
175 }
176
177 override bool opEquals(const Object object) @safe const {
178
179 return this is object;
180
181 }
182
183 }
184
185 unittest {
186
187 import fluid;
188 import std.format;
189 import std.algorithm;
190
191 const viewportSize = Vector2(100, 100);
192
193 auto io = new HeadlessBackend(viewportSize);
194 auto root = vframe(
195 layout!(1, "fill"),
196 );
197
198 root.io = io;
199
200 // First frame: Solid border on one side only
201 with (Rule)
202 root.theme = nullTheme.derive(
203 rule!Frame(
204 border.sideBottom = 4,
205 borderStyle = colorBorder(color!"018b8d"),
206 )
207 );
208 root.draw();
209
210 assert(
211 io.rectangles.find!(a => a.isClose(0, 100 - 4, 100, 4))
212 .front.color == color!"018b8d",
213 "Border must be present underneath the rectangle"
214 );
215
216 Color[4] borderColor = [color!"018b8d", color!"8d7006", color!"038d23", color!"6b048d"];
217
218 // Second frame: Border on all sides
219 // TODO optimize monochrome borders, and test them as well
220 io.nextFrame;
221
222 with (Rule)
223 root.theme = nullTheme.derive(
224 rule!Frame(
225 border = 4,
226 borderStyle = colorBorder(borderColor),
227 )
228 );
229 root.draw();
230
231 // Rectangles
232 io.assertRectangle(Rectangle(0, 4, 4, 92), borderColor.sideLeft);
233 io.assertRectangle(Rectangle(96, 4, 4, 92), borderColor.sideRight);
234 io.assertRectangle(Rectangle(4, 0, 92, 4), borderColor.sideTop);
235 io.assertRectangle(Rectangle(4, 96, 92, 4), borderColor.sideBottom);
236
237 // Triangles
238 io.assertTriangle(Vector2(0, 100), Vector2(4, 96), Vector2(0, 96), borderColor.sideLeft);
239 io.assertTriangle(Vector2(0, 0), Vector2(0, 4), Vector2(4, 4), borderColor.sideLeft);
240 io.assertTriangle(Vector2(100, 0), Vector2(96, 4), Vector2(100, 4), borderColor.sideRight);
241 io.assertTriangle(Vector2(100, 100), Vector2(100, 96), Vector2(96, 96), borderColor.sideRight);
242 io.assertTriangle(Vector2(0, 0), Vector2(4, 4), Vector2(4, 0), borderColor.sideTop);
243 io.assertTriangle(Vector2(100, 0), Vector2(96, 0), Vector2(96, 4), borderColor.sideTop);
244 io.assertTriangle(Vector2(100, 100), Vector2(96, 96), Vector2(96, 100), borderColor.sideBottom);
245 io.assertTriangle(Vector2(0, 100), Vector2(4, 100), Vector2(4, 96), borderColor.sideBottom);
246
247 }