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 }