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