VisualTheme.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. // zlib open source license
  2. //
  3. // Copyright (c) 2018 to 2022 David Forsgren Piuva
  4. //
  5. // This software is provided 'as-is', without any express or implied
  6. // warranty. In no event will the authors be held liable for any damages
  7. // arising from the use of this software.
  8. //
  9. // Permission is granted to anyone to use this software for any purpose,
  10. // including commercial applications, and to alter it and redistribute it
  11. // freely, subject to the following restrictions:
  12. //
  13. // 1. The origin of this software must not be misrepresented; you must not
  14. // claim that you wrote the original software. If you use this software
  15. // in a product, an acknowledgment in the product documentation would be
  16. // appreciated but is not required.
  17. //
  18. // 2. Altered source versions must be plainly marked as such, and must not be
  19. // misrepresented as being the original software.
  20. //
  21. // 3. This notice may not be removed or altered from any source
  22. // distribution.
  23. #include <stdint.h>
  24. #include "VisualTheme.h"
  25. #include "../api/imageAPI.h"
  26. #include "../api/drawAPI.h"
  27. #include "../api/mediaMachineAPI.h"
  28. #include "../api/configAPI.h"
  29. #include "../persistent/atomic/PersistentImage.h"
  30. namespace dsr {
  31. // The default theme
  32. // Copy, modify and compile with theme_create to get a custom theme
  33. static const ReadableString defaultMediaMachineCode =
  34. UR"QUOTE(
  35. # Helper methods
  36. BEGIN: generate_rounded_rectangle
  37. # Dimensions of the result image.
  38. INPUT: FixedPoint, width
  39. INPUT: FixedPoint, height
  40. # The subtracted offset from the radius to create a border on certain channels.
  41. INPUT: FixedPoint, border
  42. # The whole pixel radius from center points to the end of the image.
  43. INPUT: FixedPoint, rounding
  44. # Create the result image.
  45. OUTPUT: ImageU8, resultImage
  46. CREATE: resultImage, width, height
  47. # Limit outer radius to half of the image's minimum dimension.
  48. MIN: radius<FixedPoint>, width, height
  49. MUL: radius, radius, 0.5
  50. MIN: radius, radius, rounding
  51. ROUND: radius, radius
  52. # Place the inner radius for drawing.
  53. SUB: innerRadius<FixedPoint>, rounding, border
  54. # Use +- 0.5 pixel offsets for fake anti-aliasing.
  55. ADD: radiusOut<FixedPoint>, innerRadius, 0.5
  56. ADD: radiusIn<FixedPoint>, innerRadius, -0.5
  57. # Calculate dimensions for drawing.
  58. SUB: w2<FixedPoint>, width, radius
  59. SUB: w3<FixedPoint>, w2, radius
  60. SUB: w4<FixedPoint>, width, border
  61. SUB: w4, w4, border
  62. SUB: h2<FixedPoint>, height, radius
  63. SUB: h3<FixedPoint>, h2, radius
  64. SUB: r2<FixedPoint>, radius, border
  65. # Draw.
  66. FADE_REGION_RADIAL: resultImage, 0, 0, radius, radius, radius, radius, radiusIn, 255, radiusOut, 0
  67. FADE_REGION_RADIAL: resultImage, w2, 0, radius, radius, 0, radius, radiusIn, 255, radiusOut, 0
  68. FADE_REGION_RADIAL: resultImage, 0, h2, radius, radius, radius, 0, radiusIn, 255, radiusOut, 0
  69. FADE_REGION_RADIAL: resultImage, w2, h2, radius, radius, 0, 0, radiusIn, 255, radiusOut, 0
  70. RECTANGLE: resultImage, radius, border, w3, r2, 255
  71. RECTANGLE: resultImage, radius, h2, w3, r2, 255
  72. RECTANGLE: resultImage, border, radius, w4, h3, 255
  73. END:
  74. BEGIN: generate_rounded_button
  75. INPUT: FixedPoint, width
  76. INPUT: FixedPoint, height
  77. INPUT: FixedPoint, red
  78. INPUT: FixedPoint, green
  79. INPUT: FixedPoint, blue
  80. INPUT: FixedPoint, pressed
  81. INPUT: FixedPoint, border
  82. INPUT: FixedPoint, rounding
  83. OUTPUT: ImageRgbaU8, resultImage
  84. # Scale by 2 / 255 so that 127.5 represents full intensity in patternImage.
  85. MUL: normRed<FixedPoint>, red, 0.007843138
  86. MUL: normGreen<FixedPoint>, green, 0.007843138
  87. MUL: normBlue<FixedPoint>, blue, 0.007843138
  88. CREATE: patternImage<ImageU8>, width, height
  89. MUL: pressDarknessHigh<FixedPoint>, pressed, 80
  90. MUL: pressDarknessLow<FixedPoint>, pressed, 10
  91. SUB: highLuma<FixedPoint>, 150, pressDarknessHigh
  92. SUB: lowLuma<FixedPoint>, 100, pressDarknessLow
  93. FADE_LINEAR: patternImage, 0, 0, highLuma, 0, height, lowLuma
  94. CALL: generate_rounded_rectangle, lumaImage<ImageU8>, width, height, border, rounding
  95. MUL: lumaImage, lumaImage, patternImage, 0.003921569
  96. CALL: generate_rounded_rectangle, visImage<ImageU8>, width, height, 0, rounding
  97. MUL: redImage<ImageU8>, lumaImage, normRed
  98. MUL: greenImage<ImageU8>, lumaImage, normGreen
  99. MUL: blueImage<ImageU8>, lumaImage, normBlue
  100. PACK_RGBA: resultImage, redImage, greenImage, blueImage, visImage
  101. END:
  102. BEGIN: Button
  103. INPUT: FixedPoint, width
  104. INPUT: FixedPoint, height
  105. INPUT: FixedPoint, red
  106. INPUT: FixedPoint, green
  107. INPUT: FixedPoint, blue
  108. INPUT: FixedPoint, pressed
  109. INPUT: FixedPoint, border
  110. INPUT: FixedPoint, rounding
  111. OUTPUT: ImageRgbaU8, colorImage
  112. CALL: generate_rounded_button, colorImage, width, height, red, green, blue, pressed, border, rounding
  113. END:
  114. BEGIN: ListBox
  115. INPUT: FixedPoint, width
  116. INPUT: FixedPoint, height
  117. INPUT: FixedPoint, red
  118. INPUT: FixedPoint, green
  119. INPUT: FixedPoint, blue
  120. INPUT: FixedPoint, border
  121. OUTPUT: ImageRgbaU8, colorImage
  122. CREATE: colorImage, width, height
  123. ADD: b2<FixedPoint>, border, border
  124. SUB: w2<FixedPoint>, width, b2
  125. SUB: h2<FixedPoint>, height, b2
  126. RECTANGLE: colorImage, border, border, w2, h2, red, green, blue, 255
  127. END:
  128. BEGIN: VerticalScrollList
  129. INPUT: FixedPoint, width
  130. INPUT: FixedPoint, height
  131. INPUT: FixedPoint, red
  132. INPUT: FixedPoint, green
  133. INPUT: FixedPoint, blue
  134. OUTPUT: ImageRgbaU8, colorImage
  135. CREATE: visImage<ImageU8>, width, height
  136. CREATE: lumaImage<ImageU8>, width, height
  137. FADE_LINEAR: visImage, 0, 0, 128, width, 0, 0
  138. PACK_RGBA: colorImage, 0, 0, 0, visImage
  139. END:
  140. BEGIN: Panel
  141. INPUT: FixedPoint, width
  142. INPUT: FixedPoint, height
  143. INPUT: FixedPoint, red
  144. INPUT: FixedPoint, green
  145. INPUT: FixedPoint, blue
  146. INPUT: FixedPoint, border
  147. OUTPUT: ImageRgbaU8, colorImage
  148. CREATE: colorImage, width, height
  149. ADD: b2<FixedPoint>, border, border
  150. SUB: w2<FixedPoint>, width, b2
  151. SUB: h2<FixedPoint>, height, b2
  152. RECTANGLE: colorImage, border, border, w2, h2, red, green, blue, 255
  153. END:
  154. )QUOTE";
  155. // Using *.ini files for storing style settings as a simple start.
  156. // A more advanced system will be used later.
  157. static const ReadableString defaultStyleSettings =
  158. UR"QUOTE(
  159. border = 2
  160. ; Fall back on the Button method if a component's class could not be recognized.
  161. method = "Button"
  162. [Button]
  163. rounding = 12
  164. [ListBox]
  165. method = "ListBox"
  166. [VerticalScrollKnob]
  167. rounding = 8
  168. [VerticalScrollList]
  169. method = "VerticalScrollList"
  170. [ScrollUp]
  171. rounding = 5
  172. [ScrollDown]
  173. rounding = 5
  174. [Panel]
  175. border = 1
  176. method = "Panel"
  177. )QUOTE";
  178. template <typename V>
  179. struct KeywordEntry {
  180. String key;
  181. V value;
  182. KeywordEntry(const ReadableString &key, const V &value)
  183. : key(key), value(value) {}
  184. };
  185. #define FOR_EACH_COLLECTION(LOCATION, MACRO_NAME) \
  186. MACRO_NAME(LOCATION colorImages) \
  187. MACRO_NAME(LOCATION scalars) \
  188. MACRO_NAME(LOCATION strings)
  189. #define RETURN_TRUE_IF_SETTING_EXISTS(COLLECTION) \
  190. for (int64_t i = 0; i < COLLECTION.length(); i++) { \
  191. if (string_caseInsensitiveMatch(COLLECTION[i].key, key)) { \
  192. return true; \
  193. } \
  194. }
  195. struct ClassSettings {
  196. String className; // Following a line with [className] in the *.ini configuration file.
  197. List<KeywordEntry<PersistentImage>> colorImages;
  198. List<KeywordEntry<FixedPoint>> scalars;
  199. List<KeywordEntry<String>> strings;
  200. ClassSettings(const ReadableString &className)
  201. : className(className) {}
  202. bool keyExists(const ReadableString &key) {
  203. FOR_EACH_COLLECTION(this->, RETURN_TRUE_IF_SETTING_EXISTS)
  204. return false;
  205. }
  206. void setVariable(const ReadableString &key, const ReadableString &value) {
  207. if (this->keyExists(key)) { throwError(U"The property ", key, U" was defined multiple times in ", className, U"\n"); }
  208. DsrChar firstCharacter = value[0];
  209. if (firstCharacter == U'\"') {
  210. // Key = "text"
  211. this->strings.pushConstruct(key, string_unmangleQuote(value));
  212. } else {
  213. int64_t pipeIndex = string_findFirst(value, U'|');
  214. if (pipeIndex > -1 && string_caseInsensitiveMatch(string_before(value, pipeIndex), U"RGBA")) {
  215. // Key = RGBA|File:Path
  216. // Key = RGBA|WxH:Hexadecimals
  217. PersistentImage newImage;
  218. newImage.assignValue(string_after(value, pipeIndex));
  219. this->colorImages.pushConstruct(key, newImage);
  220. } else {
  221. // Key = Integer
  222. // Key = Integer.Decimals
  223. this->scalars.pushConstruct(key, FixedPoint::fromText(value));
  224. }
  225. }
  226. }
  227. // Post-condition: Returns true iff the key was found for the expected type.
  228. // Side-effect: Writes the value of the found key iff found.
  229. bool getString(String &target, const ReadableString &key) {
  230. for (int64_t i = 0; i < this->strings.length(); i++) {
  231. if (string_caseInsensitiveMatch(this->strings[i].key, key)) {
  232. target = this->strings[i].value;
  233. return true;
  234. }
  235. }
  236. return false;
  237. }
  238. };
  239. // TODO: Make it easy for visual components to ask the theme for additional resources such as custom fonts, text offset from pressing buttons and fixed dimensions for scroll lists to match fixed-size images.
  240. class VisualThemeImpl {
  241. public:
  242. MediaMachine machine;
  243. List<ClassSettings> settings;
  244. int32_t getClassIndex(const ReadableString& className) {
  245. for (int64_t i = 0; i < this->settings.length(); i++) { if (string_caseInsensitiveMatch(this->settings[i].className, className)) { return i; } }
  246. settings.pushConstruct(className);
  247. return settings.length() - 1;
  248. }
  249. VisualThemeImpl(const ReadableString& mediaCode, const ReadableString &styleSettings) : machine(machine_create(mediaCode)) {
  250. this->settings.pushConstruct(U"default");
  251. config_parse_ini(styleSettings, [this](const ReadableString& block, const ReadableString& key, const ReadableString& value) {
  252. int32_t classIndex = (string_length(block) == 0) ? 0 : this->getClassIndex(block);
  253. this->settings[classIndex].setVariable(key, value);
  254. });
  255. }
  256. // Destructor
  257. virtual ~VisualThemeImpl() {}
  258. };
  259. static VisualTheme defaultTheme;
  260. VisualTheme theme_getDefault() {
  261. if (!(defaultTheme.get())) {
  262. defaultTheme = theme_create(defaultMediaMachineCode, defaultStyleSettings);
  263. }
  264. return defaultTheme;
  265. }
  266. VisualTheme theme_create(const ReadableString &mediaCode, const ReadableString &styleSettings) {
  267. return std::make_shared<VisualThemeImpl>(mediaCode, styleSettings);
  268. }
  269. MediaMethod theme_getScalableImage(const VisualTheme &theme, const ReadableString &className) {
  270. if (!theme.get()) {
  271. throwError(U"theme_getScalableImage: Can't get scalable image from a non-existing theme!\n");
  272. }
  273. int classIndex = theme->getClassIndex(className);
  274. if (classIndex == -1) {
  275. throwError(U"theme_getScalableImage: Can't find any style class named ", className, U" in the given theme!\n");
  276. }
  277. // Try to get the method's name from the component's class settings,
  278. // and fall back on the class name itself if not found in neither the class settings nor the common default settings.
  279. String methodName;
  280. if (!theme->settings[classIndex].getString(methodName, U"method")) {
  281. if (!theme->settings[0].getString(methodName, U"method")) {
  282. throwError(U"The property \"method\" could not be found from the style class ", className, U", nor in the default settings!\n");
  283. }
  284. }
  285. return machine_getMethod(theme->machine, methodName, classIndex);
  286. }
  287. static bool assignMediaMachineArguments(ClassSettings settings, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
  288. // Search for argumentName in colorImages.
  289. for (int64_t i = 0; i < settings.colorImages.length(); i++) {
  290. if (string_caseInsensitiveMatch(settings.colorImages[i].key, argumentName)) {
  291. machine_setInputByIndex(machine, methodIndex, inputIndex, settings.colorImages[i].value.value);
  292. return true;
  293. }
  294. }
  295. // Search for argumentName in scalars.
  296. for (int64_t i = 0; i < settings.scalars.length(); i++) {
  297. if (string_caseInsensitiveMatch(settings.scalars[i].key, argumentName)) {
  298. machine_setInputByIndex(machine, methodIndex, inputIndex, settings.scalars[i].value);
  299. return true;
  300. }
  301. }
  302. return false;
  303. }
  304. bool theme_assignMediaMachineArguments(const VisualTheme &theme, int32_t contextIndex, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
  305. if (!theme.get()) { return false; }
  306. // Check in the context first.
  307. if (contextIndex > 0 && assignMediaMachineArguments(theme->settings[contextIndex], machine, methodIndex, inputIndex, argumentName)) {
  308. return true;
  309. } else {
  310. // If not found in the context, check in the default settings.
  311. return assignMediaMachineArguments(theme->settings[0], machine, methodIndex, inputIndex, argumentName);
  312. }
  313. }
  314. }