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 }