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 }