1 /** 2 * Module for style handling/parsing. 3 * 4 * Authors: 5 * Jacob Jensen 6 * License: 7 * https://github.com/PoisonEngine/poison-ui/blob/master/LICENSE 8 */ 9 module poison.ui.styles; 10 11 import std.file : readText; 12 import std.json : parseJSON, JSONValue; 13 import std.conv : to; 14 import std.array : split, replace; 15 import std..string : toLower, stripLeft, stripRight; 16 import std.algorithm : countUntil; 17 import std.base64 : Base64; 18 19 import poison.ui.graphics; 20 import poison.ui.paint; 21 import poison.ui.picture; 22 import poison.ui.fonts; 23 import poison.core : Point, Size; 24 25 /// Table of styles. 26 private Graphics[string] _styles; 27 28 /** 29 * Gets a style entry. 30 * Params: 31 * selector = The selector to find the style for. 32 */ 33 Graphics getStyleEntry(string selector) { 34 return _styles ? _styles.get(selector, null) : null; 35 } 36 37 /** 38 * Loads a style sheet. 39 * Params: 40 * The style sheet to load. 41 */ 42 void loadStyleSheet(string styleSheet) { 43 auto jsonText = readText(styleSheet); 44 45 loadStyles(jsonText); 46 } 47 48 /** 49 * Loads styles from json structured pss. 50 * Params: 51 * jsonText = The json. 52 */ 53 void loadStyles(string jsonText) { 54 if (!jsonText) { 55 return; 56 } 57 58 auto json = parseJSON("{ " ~ jsonText ~ " }"); 59 auto entries = json.object; 60 61 if (!entries) { 62 return; 63 } 64 65 foreach (selector,entryJson; entries) { 66 auto styles = entryJson.object; 67 68 if (styles) { 69 auto graphics = new Graphics(); 70 71 foreach (styleName,styleValue; styles) { 72 final switch (styleName) { 73 case "background": { 74 auto value = styleValue.str; 75 76 handleGroups(graphics, value, (gfx, propertyName, propertyValue) { 77 final switch (propertyName) { 78 case "color": { 79 gfx.backgroundPaint = handlePaintInput(propertyValue); 80 break; 81 } 82 83 case "image": { 84 gfx.backgroundPicture = handleImageInput(graphics, propertyValue); 85 break; 86 } 87 } 88 }); 89 break; 90 } 91 92 case "background-color": { 93 auto value = styleValue.str; 94 95 graphics.backgroundPaint = handlePaintInput(value); 96 break; 97 } 98 99 case "background-image": { 100 auto value = styleValue.str; 101 102 graphics.backgroundPicture = handleImageInput(graphics, value); 103 break; 104 } 105 106 case "foreground-color": { 107 auto value = styleValue.str; 108 109 graphics.foregroundPaint = handlePaintInput(value); 110 break; 111 } 112 113 case "font": { 114 auto value = styleValue.str; 115 116 handleGroups(graphics, value, (gfx, propertyName, propertyValue) { 117 final switch (propertyName) { 118 case "name": { 119 gfx.font = handleFontInput(propertyValue); 120 break; 121 } 122 123 case "path": { 124 gfx.font = loadFont(propertyValue); 125 break; 126 } 127 128 case "size": { 129 gfx.fontSize = to!uint(propertyValue); 130 break; 131 } 132 } 133 }); 134 break; 135 } 136 137 case "font-name": { 138 auto value = styleValue.str; 139 140 graphics.font = handleFontInput(value); 141 break; 142 } 143 144 case "font-path": { 145 auto value = styleValue.str; 146 147 graphics.font = loadFont(value); 148 break; 149 } 150 151 case "font-size": { 152 auto value = styleValue.str; 153 154 graphics.fontSize = to!uint(value); 155 break; 156 } 157 158 case "size": { 159 auto size = styleValue.str.split(";"); 160 161 graphics.size = new Size(to!size_t(size[0]), to!size_t(size[1])); 162 graphics.hasSize = true; 163 break; 164 } 165 166 case "position": { 167 auto position = styleValue.str.split(";"); 168 169 graphics.position = new Point(to!ptrdiff_t(position[0]), to!ptrdiff_t(position[1])); 170 graphics.hasPosition = true; 171 break; 172 } 173 174 case "paint-color": { 175 auto colors = styleValue.array; 176 177 if (!colors) { 178 break; 179 } 180 181 foreach (color; colors) { 182 Point colorPosition; 183 Size colorSize; 184 Paint colorPaint; 185 186 handleGroups(graphics, color.str, (gfx, propertyName, propertyValue) { 187 final switch (propertyName) { 188 case "position": { 189 auto position = propertyValue.split(";"); 190 191 if (position.length != 2) { 192 break; 193 } 194 195 colorPosition = new Point(to!ptrdiff_t(position[0]), to!ptrdiff_t(position[1])); 196 break; 197 } 198 199 case "size": { 200 auto size = propertyValue.split(";"); 201 202 if (size.length != 2) { 203 break; 204 } 205 206 colorSize = new Size(to!size_t(size[0]), to!size_t(size[1])); 207 break; 208 } 209 210 case "color": { 211 colorPaint = handlePaintInput(propertyValue); 212 break; 213 } 214 } 215 }); 216 217 graphics.finalizePaint(colorPosition, colorSize, colorPaint); 218 } 219 break; 220 } 221 222 case "paint-gradient-hoz": { 223 handleGradientInput(styleValue, graphics, false); 224 break; 225 } 226 227 case "paint-gradient-ver": { 228 handleGradientInput(styleValue, graphics, true); 229 break; 230 } 231 } 232 } 233 234 if (graphics.backgroundPicture) { 235 graphics.backgroundPicture.finalize(); 236 } 237 238 _styles[selector] = graphics; 239 } 240 } 241 242 import poison.core : Application; 243 244 auto app = Application.app; 245 246 if (!app) { 247 return; 248 } 249 250 app.updateStyles(); 251 } 252 253 private: 254 /** 255 * Handles group values. 256 * Params: 257 * graphics = The graphics to handle values for. 258 * value = The value to parse groups from. 259 * predicate = A predicate to mutate the graphics. 260 */ 261 void handleGroups(Graphics graphics, string value, void delegate(Graphics, string, string) predicate) { 262 assert(predicate !is null); 263 264 auto groups = value.split("|"); 265 266 foreach (group; groups) { 267 auto separatorIndex = countUntil(group, ":"); 268 auto propertyName = group[0 .. separatorIndex]; 269 auto propertyValue = group[separatorIndex + 1 .. $].stripLeft().stripRight(); 270 271 predicate(graphics, propertyName, propertyValue); 272 } 273 } 274 275 /** 276 * Handles group values. 277 * Params: 278 * graphics = The graphics to handle values for. 279 * value = The value to parse groups from. 280 * predicate = A predicate to mutate the graphics. 281 */ 282 void handleGroups(Graphics graphics, string value, void function(Graphics, string, string) predicate) { 283 assert(predicate !is null); 284 285 auto groups = value.split("|"); 286 287 foreach (group; groups) { 288 auto separatorIndex = countUntil(group, ":"); 289 auto propertyName = group[0 .. separatorIndex]; 290 auto propertyValue = group[separatorIndex + 1 .. $]; 291 292 predicate(graphics, propertyName, propertyValue); 293 } 294 } 295 296 /** 297 * Handles gradient input. 298 * Params: 299 * styleValue = The value of the style input. 300 * graphics = The graphics. 301 * isVertical = Boolean determining whether the gradient is vertical or not. 302 */ 303 void handleGradientInput(JSONValue styleValue, Graphics graphics, bool isVertical) { 304 auto gradients = styleValue.array; 305 306 if (!gradients) { 307 return; 308 } 309 310 foreach (gradient; gradients) { 311 Point gradientPosition; 312 Size gradientSize; 313 Paint fromColorPaint; 314 Paint toColorPaint; 315 316 handleGroups(graphics, gradient.str, (gfx, propertyName, propertyValue) { 317 final switch (propertyName) { 318 case "position": { 319 auto position = propertyValue.split(";"); 320 321 if (position.length != 2) { 322 break; 323 } 324 325 gradientPosition = new Point(to!ptrdiff_t(position[0]), to!ptrdiff_t(position[1])); 326 break; 327 } 328 329 case "size": { 330 auto size = propertyValue.split(";"); 331 332 if (size.length != 2) { 333 break; 334 } 335 336 gradientSize = new Size(to!size_t(size[0]), to!size_t(size[1])); 337 break; 338 } 339 340 case "fromColor": { 341 fromColorPaint = handlePaintInput(propertyValue); 342 break; 343 } 344 345 case "toColor": { 346 toColorPaint = handlePaintInput(propertyValue); 347 break; 348 } 349 } 350 }); 351 352 if (isVertical) { 353 graphics.finalizeGradientVertical(gradientPosition, gradientSize, fromColorPaint, toColorPaint); 354 } 355 else { 356 graphics.finalizeGradientHorizontal(gradientPosition, gradientSize, fromColorPaint, toColorPaint); 357 } 358 } 359 } 360 361 /** 362 * Handles paint input. 363 * Params: 364 * value = The value of the paint input. 365 * Returns: 366 * The generated paint from the value. 367 */ 368 Paint handlePaintInput(string value) { 369 if (value[0] == '#') { 370 value = value[1 .. $].toLower(); 371 372 if (value.length == 3 && value[0] == value[1] && value[0] == value[2]) { 373 value ~= value; 374 } 375 376 if (value.length == 6) { 377 value ~= "ff"; 378 } 379 380 auto values = [value[0 .. 2], value[2 .. 4], value[4 .. 6], value[6 .. $]]; 381 382 ubyte r = to!ubyte(values[0], 16); 383 ubyte g = to!ubyte(values[1], 16); 384 ubyte b = to!ubyte(values[2], 16); 385 ubyte a = to!ubyte(values[3], 16); 386 387 return paintFromRGBA(r, g, b, a); 388 } 389 else { 390 auto values = value.split(";"); 391 392 if (values.length > 1) { 393 assert(values.length == 3 || values.length == 4); 394 395 ubyte r = to!ubyte(values[0]); 396 ubyte g = to!ubyte(values[1]); 397 ubyte b = to!ubyte(values[2]); 398 ubyte a = 0xff; 399 400 if (values.length == 4) { 401 a = to!ubyte(values[3]); 402 } 403 404 return paintFromRGBA(r, g, b, a); 405 } 406 407 return paintFromName(value); 408 } 409 } 410 411 /** 412 * Handles image inputs. 413 * Params: 414 * graphics = The graphics. 415 * value = The image input. 416 * Returns: 417 * The generated picture from the value. 418 */ 419 Picture handleImageInput(Graphics graphics, string value) { 420 auto data = value.split(":"); 421 422 if (data.length == 1) { 423 if (graphics.backgroundPicture) { 424 graphics.backgroundPicture.fileName = value; 425 return graphics.backgroundPicture; 426 } 427 428 return new Picture(value); 429 } 430 else { 431 assert(data.length == 2); 432 433 auto propertyName = data[0]; 434 auto propertyValue = data[1]; 435 436 final switch (propertyName) { 437 case "base64": { 438 auto buffer = Base64.decode(propertyValue); 439 440 if (graphics.backgroundPicture) { 441 graphics.backgroundPicture.imageBuffer = buffer; 442 return graphics.backgroundPicture; 443 } 444 445 return new Picture(buffer); 446 } 447 } 448 } 449 } 450 451 /** 452 * Handles font input. 453 * Params: 454 * value = The font input value. 455 */ 456 Font handleFontInput(string value) { 457 auto values = value.split(";"); 458 auto fontName = values[0]; 459 460 if (values.length == 2) { 461 return retrieveFont(fontName, to!FontStyle(values[1])); 462 } 463 464 return retrieveFont(fontName, FontStyle.normal); 465 }