1 /*
2 Minetest
3 Copyright (C) 2019 rubenwardy
4 
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14 
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19 
20 #include "client/tile.h" // ITextureSource
21 #include "client/fontengine.h"
22 #include "debug.h"
23 #include "irrlichttypes_extrabloated.h"
24 #include "util/string.h"
25 #include <algorithm>
26 #include <array>
27 #include <vector>
28 
29 #pragma once
30 
31 class StyleSpec
32 {
33 public:
34 	enum Property
35 	{
36 		TEXTCOLOR,
37 		BGCOLOR,
38 		BGCOLOR_HOVERED, // Note: Deprecated property
39 		BGCOLOR_PRESSED, // Note: Deprecated property
40 		NOCLIP,
41 		BORDER,
42 		BGIMG,
43 		BGIMG_HOVERED, // Note: Deprecated property
44 		BGIMG_MIDDLE,
45 		BGIMG_PRESSED, // Note: Deprecated property
46 		FGIMG,
47 		FGIMG_HOVERED, // Note: Deprecated property
48 		FGIMG_PRESSED, // Note: Deprecated property
49 		ALPHA,
50 		CONTENT_OFFSET,
51 		PADDING,
52 		FONT,
53 		FONT_SIZE,
54 		COLORS,
55 		BORDERCOLORS,
56 		BORDERWIDTHS,
57 		SOUND,
58 		SPACING,
59 		SIZE,
60 		NUM_PROPERTIES,
61 		NONE
62 	};
63 	enum State
64 	{
65 		STATE_DEFAULT = 0,
66 		STATE_HOVERED = 1 << 0,
67 		STATE_PRESSED = 1 << 1,
68 		NUM_STATES = 1 << 2,
69 		STATE_INVALID = 1 << 3,
70 	};
71 
72 private:
73 	std::array<bool, NUM_PROPERTIES> property_set{};
74 	std::array<std::string, NUM_PROPERTIES> properties;
75 	State state_map = STATE_DEFAULT;
76 
77 public:
GetPropertyByName(const std::string & name)78 	static Property GetPropertyByName(const std::string &name)
79 	{
80 		if (name == "textcolor") {
81 			return TEXTCOLOR;
82 		} else if (name == "bgcolor") {
83 			return BGCOLOR;
84 		} else if (name == "bgcolor_hovered") {
85 			return BGCOLOR_HOVERED;
86 		} else if (name == "bgcolor_pressed") {
87 			return BGCOLOR_PRESSED;
88 		} else if (name == "noclip") {
89 			return NOCLIP;
90 		} else if (name == "border") {
91 			return BORDER;
92 		} else if (name == "bgimg") {
93 			return BGIMG;
94 		} else if (name == "bgimg_hovered") {
95 			return BGIMG_HOVERED;
96 		} else if (name == "bgimg_middle") {
97 			return BGIMG_MIDDLE;
98 		} else if (name == "bgimg_pressed") {
99 			return BGIMG_PRESSED;
100 		} else if (name == "fgimg") {
101 			return FGIMG;
102 		} else if (name == "fgimg_hovered") {
103 			return FGIMG_HOVERED;
104 		} else if (name == "fgimg_pressed") {
105 			return FGIMG_PRESSED;
106 		} else if (name == "alpha") {
107 			return ALPHA;
108 		} else if (name == "content_offset") {
109 			return CONTENT_OFFSET;
110 		} else if (name == "padding") {
111 			return PADDING;
112 		} else if (name == "font") {
113 			return FONT;
114 		} else if (name == "font_size") {
115 			return FONT_SIZE;
116 		} else if (name == "colors") {
117 			return COLORS;
118 		} else if (name == "bordercolors") {
119 			return BORDERCOLORS;
120 		} else if (name == "borderwidths") {
121 			return BORDERWIDTHS;
122 		} else if (name == "sound") {
123 			return SOUND;
124 		} else if (name == "spacing") {
125 			return SPACING;
126 		} else if (name == "size") {
127 			return SIZE;
128 		} else {
129 			return NONE;
130 		}
131 	}
132 
get(Property prop,std::string def)133 	std::string get(Property prop, std::string def) const
134 	{
135 		const auto &val = properties[prop];
136 		return val.empty() ? def : val;
137 	}
138 
set(Property prop,const std::string & value)139 	void set(Property prop, const std::string &value)
140 	{
141 		properties[prop] = value;
142 		property_set[prop] = true;
143 	}
144 
145 	//! Parses a name and returns the corresponding state enum
getStateByName(const std::string & name)146 	static State getStateByName(const std::string &name)
147 	{
148 		if (name == "default") {
149 			return STATE_DEFAULT;
150 		} else if (name == "hovered") {
151 			return STATE_HOVERED;
152 		} else if (name == "pressed") {
153 			return STATE_PRESSED;
154 		} else {
155 			return STATE_INVALID;
156 		}
157 	}
158 
159 	//! Gets the state that this style is intended for
getState()160 	State getState() const
161 	{
162 		return state_map;
163 	}
164 
165 	//! Set the given state on this style
addState(State state)166 	void addState(State state)
167 	{
168 		FATAL_ERROR_IF(state >= NUM_STATES, "Out-of-bounds state received");
169 
170 		state_map = static_cast<State>(state_map | state);
171 	}
172 
173 	//! Using a list of styles mapped to state values, calculate the final
174 	//  combined style for a state by propagating values in its component states
getStyleFromStatePropagation(const std::array<StyleSpec,NUM_STATES> & styles,State state)175 	static StyleSpec getStyleFromStatePropagation(const std::array<StyleSpec, NUM_STATES> &styles, State state)
176 	{
177 		StyleSpec temp = styles[StyleSpec::STATE_DEFAULT];
178 		temp.state_map = state;
179 		for (int i = StyleSpec::STATE_DEFAULT + 1; i <= state; i++) {
180 			if ((state & i) != 0) {
181 				temp = temp | styles[i];
182 			}
183 		}
184 
185 		return temp;
186 	}
187 
getColor(Property prop,video::SColor def)188 	video::SColor getColor(Property prop, video::SColor def) const
189 	{
190 		const auto &val = properties[prop];
191 		if (val.empty()) {
192 			return def;
193 		}
194 
195 		parseColorString(val, def, false, 0xFF);
196 		return def;
197 	}
198 
getColor(Property prop)199 	video::SColor getColor(Property prop) const
200 	{
201 		const auto &val = properties[prop];
202 		FATAL_ERROR_IF(val.empty(), "Unexpected missing property");
203 
204 		video::SColor color;
205 		parseColorString(val, color, false, 0xFF);
206 		return color;
207 	}
208 
getColorArray(Property prop,std::array<video::SColor,4> def)209 	std::array<video::SColor, 4> getColorArray(Property prop,
210 		std::array<video::SColor, 4> def) const
211 	{
212 		const auto &val = properties[prop];
213 		if (val.empty())
214 			return def;
215 
216 		std::vector<std::string> strs;
217 		if (!parseArray(val, strs))
218 			return def;
219 
220 		for (size_t i = 0; i <= 3; i++) {
221 			video::SColor color;
222 			if (parseColorString(strs[i], color, false, 0xff))
223 				def[i] = color;
224 		}
225 
226 		return def;
227 	}
228 
getIntArray(Property prop,std::array<s32,4> def)229 	std::array<s32, 4> getIntArray(Property prop, std::array<s32, 4> def) const
230 	{
231 		const auto &val = properties[prop];
232 		if (val.empty())
233 			return def;
234 
235 		std::vector<std::string> strs;
236 		if (!parseArray(val, strs))
237 			return def;
238 
239 		for (size_t i = 0; i <= 3; i++)
240 			def[i] = stoi(strs[i]);
241 
242 		return def;
243 	}
244 
getRect(Property prop,irr::core::rect<s32> def)245 	irr::core::rect<s32> getRect(Property prop, irr::core::rect<s32> def) const
246 	{
247 		const auto &val = properties[prop];
248 		if (val.empty())
249 			return def;
250 
251 		irr::core::rect<s32> rect;
252 		if (!parseRect(val, &rect))
253 			return def;
254 
255 		return rect;
256 	}
257 
getRect(Property prop)258 	irr::core::rect<s32> getRect(Property prop) const
259 	{
260 		const auto &val = properties[prop];
261 		FATAL_ERROR_IF(val.empty(), "Unexpected missing property");
262 
263 		irr::core::rect<s32> rect;
264 		parseRect(val, &rect);
265 		return rect;
266 	}
267 
getVector2f(Property prop,v2f32 def)268 	v2f32 getVector2f(Property prop, v2f32 def) const
269 	{
270 		const auto &val = properties[prop];
271 		if (val.empty())
272 			return def;
273 
274 		v2f32 vec;
275 		if (!parseVector2f(val, &vec))
276 			return def;
277 
278 		return vec;
279 	}
280 
getVector2i(Property prop,v2s32 def)281 	v2s32 getVector2i(Property prop, v2s32 def) const
282 	{
283 		const auto &val = properties[prop];
284 		if (val.empty())
285 			return def;
286 
287 		v2f32 vec;
288 		if (!parseVector2f(val, &vec))
289 			return def;
290 
291 		return v2s32(vec.X, vec.Y);
292 	}
293 
getVector2i(Property prop)294 	v2s32 getVector2i(Property prop) const
295 	{
296 		const auto &val = properties[prop];
297 		FATAL_ERROR_IF(val.empty(), "Unexpected missing property");
298 
299 		v2f32 vec;
300 		parseVector2f(val, &vec);
301 		return v2s32(vec.X, vec.Y);
302 	}
303 
getFont()304 	gui::IGUIFont *getFont() const
305 	{
306 		FontSpec spec(FONT_SIZE_UNSPECIFIED, FM_Standard, false, false);
307 
308 		const std::string &font = properties[FONT];
309 		const std::string &size = properties[FONT_SIZE];
310 
311 		if (font.empty() && size.empty())
312 			return nullptr;
313 
314 		std::vector<std::string> modes = split(font, ',');
315 
316 		for (size_t i = 0; i < modes.size(); i++) {
317 			if (modes[i] == "normal")
318 				spec.mode = FM_Standard;
319 			else if (modes[i] == "mono")
320 				spec.mode = FM_Mono;
321 			else if (modes[i] == "bold")
322 				spec.bold = true;
323 			else if (modes[i] == "italic")
324 				spec.italic = true;
325 		}
326 
327 		if (!size.empty()) {
328 			int calc_size = 1;
329 
330 			if (size[0] == '*') {
331 				std::string new_size = size.substr(1); // Remove '*' (invalid for stof)
332 				calc_size = stof(new_size) * g_fontengine->getFontSize(spec.mode);
333 			} else if (size[0] == '+' || size[0] == '-') {
334 				calc_size = stoi(size) + g_fontengine->getFontSize(spec.mode);
335 			} else {
336 				calc_size = stoi(size);
337 			}
338 
339 			spec.size = (unsigned)std::min(std::max(calc_size, 1), 999);
340 		}
341 
342 		return g_fontengine->getFont(spec);
343 	}
344 
getTexture(Property prop,ISimpleTextureSource * tsrc,video::ITexture * def)345 	video::ITexture *getTexture(Property prop, ISimpleTextureSource *tsrc,
346 			video::ITexture *def) const
347 	{
348 		const auto &val = properties[prop];
349 		if (val.empty()) {
350 			return def;
351 		}
352 
353 		video::ITexture *texture = tsrc->getTexture(val);
354 
355 		return texture;
356 	}
357 
getTexture(Property prop,ISimpleTextureSource * tsrc)358 	video::ITexture *getTexture(Property prop, ISimpleTextureSource *tsrc) const
359 	{
360 		const auto &val = properties[prop];
361 		FATAL_ERROR_IF(val.empty(), "Unexpected missing property");
362 
363 		video::ITexture *texture = tsrc->getTexture(val);
364 
365 		return texture;
366 	}
367 
getBool(Property prop,bool def)368 	bool getBool(Property prop, bool def) const
369 	{
370 		const auto &val = properties[prop];
371 		if (val.empty()) {
372 			return def;
373 		}
374 
375 		return is_yes(val);
376 	}
377 
isNotDefault(Property prop)378 	inline bool isNotDefault(Property prop) const
379 	{
380 		return !properties[prop].empty();
381 	}
382 
hasProperty(Property prop)383 	inline bool hasProperty(Property prop) const { return property_set[prop]; }
384 
385 	StyleSpec &operator|=(const StyleSpec &other)
386 	{
387 		for (size_t i = 0; i < NUM_PROPERTIES; i++) {
388 			auto prop = (Property)i;
389 			if (other.hasProperty(prop)) {
390 				set(prop, other.get(prop, ""));
391 			}
392 		}
393 
394 		return *this;
395 	}
396 
397 	StyleSpec operator|(const StyleSpec &other) const
398 	{
399 		StyleSpec newspec = *this;
400 		newspec |= other;
401 		return newspec;
402 	}
403 
404 private:
parseArray(const std::string & value,std::vector<std::string> & arr)405 	bool parseArray(const std::string &value, std::vector<std::string> &arr) const
406 	{
407 		std::vector<std::string> strs = split(value, ',');
408 
409 		if (strs.size() == 1) {
410 			arr = {strs[0], strs[0], strs[0], strs[0]};
411 		} else if (strs.size() == 2) {
412 			arr = {strs[0], strs[1], strs[0], strs[1]};
413 		} else if (strs.size() == 4) {
414 			arr = strs;
415 		} else {
416 			warningstream << "Invalid array size (" << strs.size()
417 					<< " arguments): \"" << value << "\"" << std::endl;
418 			return false;
419 		}
420 		return true;
421 	}
422 
parseRect(const std::string & value,irr::core::rect<s32> * parsed_rect)423 	bool parseRect(const std::string &value, irr::core::rect<s32> *parsed_rect) const
424 	{
425 		irr::core::rect<s32> rect;
426 		std::vector<std::string> v_rect = split(value, ',');
427 
428 		if (v_rect.size() == 1) {
429 			s32 x = stoi(v_rect[0]);
430 			rect.UpperLeftCorner = irr::core::vector2di(x, x);
431 			rect.LowerRightCorner = irr::core::vector2di(-x, -x);
432 		} else if (v_rect.size() == 2) {
433 			s32 x = stoi(v_rect[0]);
434 			s32 y =	stoi(v_rect[1]);
435 			rect.UpperLeftCorner = irr::core::vector2di(x, y);
436 			rect.LowerRightCorner = irr::core::vector2di(-x, -y);
437 			// `-x` is interpreted as `w - x`
438 		} else if (v_rect.size() == 4) {
439 			rect.UpperLeftCorner = irr::core::vector2di(
440 					stoi(v_rect[0]), stoi(v_rect[1]));
441 			rect.LowerRightCorner = irr::core::vector2di(
442 					stoi(v_rect[2]), stoi(v_rect[3]));
443 		} else {
444 			warningstream << "Invalid rectangle string format: \"" << value
445 					<< "\"" << std::endl;
446 			return false;
447 		}
448 
449 		*parsed_rect = rect;
450 
451 		return true;
452 	}
453 
parseVector2f(const std::string & value,v2f32 * parsed_vec)454 	bool parseVector2f(const std::string &value, v2f32 *parsed_vec) const
455 	{
456 		v2f32 vec;
457 		std::vector<std::string> v_vector = split(value, ',');
458 
459 		if (v_vector.size() == 1) {
460 			f32 x = stof(v_vector[0]);
461 			vec.X = x;
462 			vec.Y = x;
463 		} else if (v_vector.size() == 2) {
464 			vec.X = stof(v_vector[0]);
465 			vec.Y =	stof(v_vector[1]);
466 		} else {
467 			warningstream << "Invalid 2d vector string format: \"" << value
468 					<< "\"" << std::endl;
469 			return false;
470 		}
471 
472 		*parsed_vec = vec;
473 
474 		return true;
475 	}
476 };
477