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