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 }