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 }