1 module poison.ui.styles;
2 
3 import std.file : readText;
4 import std.json : parseJSON;
5 import std.conv : to;
6 import std.array : split, replace;
7 import std.string : toLower;
8 import std.algorithm : strip, stripLeft, stripRight, countUntil;
9 import std.base64 : Base64;
10 
11 import poison.ui.graphics;
12 import poison.ui.paint;
13 import poison.ui.picture;
14 import poison.ui.fonts;
15 
16 /// Table of styles.
17 private Graphics[string] _styles;
18 
19 /**
20 * Gets a style entry.
21 * Params:
22 *   selector =  The selector to find the style for.
23 */
24 Graphics getStyleEntry(string selector) {
25   return _styles ? _styles.get(selector, null) : null;
26 }
27 
28 /**
29 * Loads a style sheet.
30 * Params:
31 *   The style sheet to load.
32 */
33 void loadStyleSheet(string styleSheet) {
34   auto jsonText = readText(styleSheet);
35 
36   loadStyles(jsonText);
37 }
38 
39 /**
40 * Loads styles from json structured pss.
41 * Params:
42 *   jsonText =  The json.
43 */
44 void loadStyles(string jsonText) {
45   if (!jsonText) {
46     return;
47   }
48 
49   auto json = parseJSON("{ " ~ jsonText ~ " }");
50   auto entries = json.object;
51 
52   if (!entries) {
53     return;
54   }
55 
56   foreach (selector,entryJson; entries) {
57     auto styles = entryJson.object;
58 
59     if (styles) {
60       auto graphics = new Graphics();
61 
62       foreach (styleName,styleValue; styles) {
63         auto value = styleValue.str;
64 
65         assert(value);
66 
67         final switch (styleName) {
68           case "background": {
69             handleGroups(graphics, value, (gfx, propertyName, propertyValue) {
70               final switch (propertyName) {
71                 case "color": {
72                   gfx.backgroundPaint = handlePaintInput(propertyValue);
73                   break;
74                 }
75 
76                 case "image": {
77                   gfx.backgroundPicture = handleImageInput(propertyValue);
78                   break;
79                 }
80               }
81             });
82             break;
83           }
84 
85           case "background-color": {
86             graphics.backgroundPaint = handlePaintInput(value);
87             break;
88           }
89 
90           case "background-image": {
91             graphics.backgroundPicture = handleImageInput(value);
92             break;
93           }
94 
95           case "foreground-color": {
96             graphics.foregroundPaint = handlePaintInput(value);
97             break;
98           }
99 
100           case "font": {
101             handleGroups(graphics, value, (gfx, propertyName, propertyValue) {
102               final switch (propertyName) {
103                 case "name": {
104                   gfx.font = handleFontInput(propertyValue);
105                   break;
106                 }
107 
108                 case "path": {
109                   gfx.font = loadFont(propertyValue);
110                   break;
111                 }
112 
113                 case "size": {
114                   gfx.fontSize = to!uint(propertyValue);
115                   break;
116                 }
117               }
118             });
119             break;
120           }
121 
122           case "font-name": {
123             graphics.font = handleFontInput(value);
124             break;
125           }
126 
127           case "font-path": {
128             graphics.font = loadFont(value);
129             break;
130           }
131 
132           case "font-size": {
133             graphics.fontSize = to!uint(value);
134           }
135         }
136       }
137 
138       _styles[selector] = graphics;
139     }
140   }
141 
142   import poison.core : Application;
143 
144   auto app = Application.app;
145 
146   if (!app) {
147     return;
148   }
149 
150   app.updateStyles();
151 }
152 
153 private:
154 /**
155 * Handles group values.
156 * Params:
157 *   graphics =  The graphics to handle values for.
158 *   value =     The value to parse groups from.
159 *   predicate = A predicate to mutate the graphics.
160 */
161 void handleGroups(Graphics graphics, string value, void delegate(Graphics, string, string) predicate) {
162   assert(predicate !is null);
163 
164   auto groups = value.split("|");
165 
166   foreach (group; groups) {
167     auto separatorIndex = countUntil(group, ":");
168     auto propertyName = group[0 .. separatorIndex];
169     auto propertyValue = group[separatorIndex + 1 .. $];
170 
171     predicate(graphics, propertyName, propertyValue);
172   }
173 }
174 
175 /**
176 * Handles group values.
177 * Params:
178 *   graphics =  The graphics to handle values for.
179 *   value =     The value to parse groups from.
180 *   predicate = A predicate to mutate the graphics.
181 */
182 void handleGroups(Graphics graphics, string value, void function(Graphics, string, string) predicate) {
183   assert(predicate !is null);
184 
185   auto groups = value.split("|");
186 
187   foreach (group; groups) {
188     auto separatorIndex = countUntil(group, ":");
189     auto propertyName = group[0 .. separatorIndex];
190     auto propertyValue = group[separatorIndex + 1 .. $];
191 
192     predicate(graphics, propertyName, propertyValue);
193   }
194 }
195 
196 /**
197 * Handles paint input.
198 * Params:
199 *   value = The value of the paint input.
200 * Returns:
201 *   The generated paint from the value.
202 */
203 Paint handlePaintInput(string value) {
204   if (value[0] == '#') {
205     value = value[1 .. $].toLower();
206 
207     if (value.length == 3 && value[0] == value[1] && value[0] == value[2]) {
208       value ~= value;
209     }
210 
211     if (value.length == 6) {
212       value ~= "ff";
213     }
214 
215     auto values = [value[0 .. 2], value[2 .. 4], value[4 .. 6], value[6 .. $]];
216 
217     ubyte r = to!ubyte(values[0], 16);
218     ubyte g = to!ubyte(values[1], 16);
219     ubyte b = to!ubyte(values[2], 16);
220     ubyte a = to!ubyte(values[3], 16);
221 
222     return paintFromRGBA(r, g, b, a);
223   }
224   else {
225     auto values = value.split(";");
226 
227     if (values.length > 1) {
228       assert(values.length == 3 || values.length == 4);
229 
230       ubyte r = to!ubyte(values[0]);
231       ubyte g = to!ubyte(values[1]);
232       ubyte b = to!ubyte(values[2]);
233       ubyte a = 0xff;
234 
235       if (values.length == 4) {
236         a = to!ubyte(values[3]);
237       }
238 
239       return paintFromRGBA(r, g, b, a);
240     }
241 
242     return paintFromName(value);
243   }
244 }
245 
246 /**
247 * Handles image inputs.
248 * Params:
249 *   value = The image input.
250 * Returns:
251 *   The generated picture from the value.
252 */
253 Picture handleImageInput(string value) {
254   auto data = value.split(":");
255 
256   if (data.length == 1) {
257     return new Picture(value);
258   }
259   else {
260     assert(data.length == 2);
261 
262     auto propertyName = data[0];
263     auto propertyValue = data[1];
264 
265     final switch (propertyName) {
266       case "base64": {
267         auto buffer = Base64.decode(propertyValue);
268 
269         return new Picture(buffer);
270       }
271     }
272   }
273 }
274 
275 /**
276 * Handles font input.
277 * Params:
278 *   value = The font input value.
279 */
280 Font handleFontInput(string value) {
281   auto values = value.split(";");
282   auto fontName = values[0];
283 
284   if (values.length == 2) {
285     return retrieveFont(fontName, to!FontStyle(values[1]));
286   }
287 
288   return retrieveFont(fontName, FontStyle.normal);
289 }