VisualTheme.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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/fileAPI.h"
  26. #include "../api/imageAPI.h"
  27. #include "../api/drawAPI.h"
  28. #include "../api/mediaMachineAPI.h"
  29. #include "../api/configAPI.h"
  30. #include "../persistent/atomic/PersistentImage.h"
  31. namespace dsr {
  32. // The default theme
  33. // Copy, modify and compile with theme_create to get a custom theme
  34. static const ReadableString defaultMediaMachineCode =
  35. UR"QUOTE(
  36. # Helper methods
  37. BEGIN: generate_rounded_rectangle
  38. # Dimensions of the result image.
  39. INPUT: FixedPoint, width
  40. INPUT: FixedPoint, height
  41. # The subtracted offset from the radius to create a border on certain channels.
  42. INPUT: FixedPoint, border
  43. # The whole pixel radius from center points to the end of the image.
  44. INPUT: FixedPoint, rounding
  45. # Create the result image.
  46. OUTPUT: ImageU8, resultImage
  47. CREATE: resultImage, width, height
  48. # Limit outer radius to half of the image's minimum dimension.
  49. MIN: radius<FixedPoint>, width, height
  50. MUL: radius, radius, 0.5
  51. MIN: radius, radius, rounding
  52. ROUND: radius, radius
  53. # Place the inner radius for drawing.
  54. SUB: innerRadius<FixedPoint>, rounding, border
  55. # Use +- 0.5 pixel offsets for fake anti-aliasing.
  56. ADD: radiusOut<FixedPoint>, innerRadius, 0.5
  57. ADD: radiusIn<FixedPoint>, innerRadius, -0.5
  58. # Calculate dimensions for drawing.
  59. SUB: w2<FixedPoint>, width, radius
  60. SUB: w3<FixedPoint>, w2, radius
  61. SUB: w4<FixedPoint>, width, border
  62. SUB: w4, w4, border
  63. SUB: h2<FixedPoint>, height, radius
  64. SUB: h3<FixedPoint>, h2, radius
  65. SUB: r2<FixedPoint>, radius, border
  66. # Draw.
  67. FADE_REGION_RADIAL: resultImage, 0, 0, radius, radius, radius, radius, radiusIn, 255, radiusOut, 0
  68. FADE_REGION_RADIAL: resultImage, w2, 0, radius, radius, 0, radius, radiusIn, 255, radiusOut, 0
  69. FADE_REGION_RADIAL: resultImage, 0, h2, radius, radius, radius, 0, radiusIn, 255, radiusOut, 0
  70. FADE_REGION_RADIAL: resultImage, w2, h2, radius, radius, 0, 0, radiusIn, 255, radiusOut, 0
  71. RECTANGLE: resultImage, radius, border, w3, r2, 255
  72. RECTANGLE: resultImage, radius, h2, w3, r2, 255
  73. RECTANGLE: resultImage, border, radius, w4, h3, 255
  74. END:
  75. BEGIN: generate_rounded_button
  76. INPUT: FixedPoint, width
  77. INPUT: FixedPoint, height
  78. INPUT: FixedPoint, red
  79. INPUT: FixedPoint, green
  80. INPUT: FixedPoint, blue
  81. INPUT: FixedPoint, pressed
  82. INPUT: FixedPoint, border
  83. INPUT: FixedPoint, rounding
  84. OUTPUT: ImageRgbaU8, resultImage
  85. # Scale by 2 / 255 so that 127.5 represents full intensity in patternImage.
  86. MUL: normRed<FixedPoint>, red, 0.007843138
  87. MUL: normGreen<FixedPoint>, green, 0.007843138
  88. MUL: normBlue<FixedPoint>, blue, 0.007843138
  89. CREATE: patternImage<ImageU8>, width, height
  90. MUL: pressDarknessHigh<FixedPoint>, pressed, 80
  91. MUL: pressDarknessLow<FixedPoint>, pressed, 10
  92. SUB: highLuma<FixedPoint>, 150, pressDarknessHigh
  93. SUB: lowLuma<FixedPoint>, 100, pressDarknessLow
  94. FADE_LINEAR: patternImage, 0, 0, highLuma, 0, height, lowLuma
  95. CALL: generate_rounded_rectangle, lumaImage<ImageU8>, width, height, border, rounding
  96. MUL: lumaImage, lumaImage, patternImage, 0.003921569
  97. CALL: generate_rounded_rectangle, visImage<ImageU8>, width, height, 0, rounding
  98. MUL: redImage<ImageU8>, lumaImage, normRed
  99. MUL: greenImage<ImageU8>, lumaImage, normGreen
  100. MUL: blueImage<ImageU8>, lumaImage, normBlue
  101. PACK_RGBA: resultImage, redImage, greenImage, blueImage, visImage
  102. END:
  103. BEGIN: Button
  104. INPUT: FixedPoint, width
  105. INPUT: FixedPoint, height
  106. INPUT: FixedPoint, red
  107. INPUT: FixedPoint, green
  108. INPUT: FixedPoint, blue
  109. INPUT: FixedPoint, pressed
  110. INPUT: FixedPoint, border
  111. INPUT: FixedPoint, rounding
  112. OUTPUT: ImageRgbaU8, colorImage
  113. CALL: generate_rounded_button, colorImage, width, height, red, green, blue, pressed, border, rounding
  114. END:
  115. BEGIN: ListBox
  116. INPUT: FixedPoint, width
  117. INPUT: FixedPoint, height
  118. INPUT: FixedPoint, red
  119. INPUT: FixedPoint, green
  120. INPUT: FixedPoint, blue
  121. INPUT: FixedPoint, border
  122. OUTPUT: ImageRgbaU8, colorImage
  123. CREATE: colorImage, width, height
  124. ADD: b2<FixedPoint>, border, border
  125. SUB: w2<FixedPoint>, width, b2
  126. SUB: h2<FixedPoint>, height, b2
  127. RECTANGLE: colorImage, border, border, w2, h2, red, green, blue, 255
  128. END:
  129. BEGIN: VerticalScrollList
  130. INPUT: FixedPoint, width
  131. INPUT: FixedPoint, height
  132. INPUT: FixedPoint, red
  133. INPUT: FixedPoint, green
  134. INPUT: FixedPoint, blue
  135. OUTPUT: ImageRgbaU8, colorImage
  136. CREATE: visImage<ImageU8>, width, height
  137. CREATE: lumaImage<ImageU8>, width, height
  138. FADE_LINEAR: visImage, 0, 0, 128, width, 0, 0
  139. PACK_RGBA: colorImage, 0, 0, 0, visImage
  140. END:
  141. BEGIN: Panel
  142. INPUT: FixedPoint, width
  143. INPUT: FixedPoint, height
  144. INPUT: FixedPoint, red
  145. INPUT: FixedPoint, green
  146. INPUT: FixedPoint, blue
  147. INPUT: FixedPoint, border
  148. OUTPUT: ImageRgbaU8, colorImage
  149. CREATE: colorImage, width, height
  150. ADD: b2<FixedPoint>, border, border
  151. SUB: w2<FixedPoint>, width, b2
  152. SUB: h2<FixedPoint>, height, b2
  153. RECTANGLE: colorImage, border, border, w2, h2, red, green, blue, 255
  154. END:
  155. )QUOTE";
  156. // Using *.ini files for storing style settings as a simple start.
  157. // A more advanced system will be used later.
  158. static const ReadableString defaultStyleSettings =
  159. UR"QUOTE(
  160. border = 2
  161. ; Fall back on the Button method if a component's class could not be recognized.
  162. method = "Button"
  163. [Button]
  164. rounding = 12
  165. [ListBox]
  166. method = "ListBox"
  167. [VerticalScrollKnob]
  168. rounding = 8
  169. [VerticalScrollList]
  170. method = "VerticalScrollList"
  171. [ScrollUp]
  172. rounding = 5
  173. [ScrollDown]
  174. rounding = 5
  175. [Panel]
  176. border = 1
  177. method = "Panel"
  178. )QUOTE";
  179. template <typename V>
  180. struct KeywordEntry {
  181. String key;
  182. V value;
  183. KeywordEntry(const ReadableString &key, const V &value)
  184. : key(key), value(value) {}
  185. };
  186. #define FOR_EACH_COLLECTION(LOCATION, MACRO_NAME) \
  187. MACRO_NAME(LOCATION colorImages) \
  188. MACRO_NAME(LOCATION scalars) \
  189. MACRO_NAME(LOCATION strings)
  190. #define RETURN_TRUE_IF_SETTING_EXISTS(COLLECTION) \
  191. for (int64_t i = 0; i < COLLECTION.length(); i++) { \
  192. if (string_caseInsensitiveMatch(COLLECTION[i].key, key)) { \
  193. return true; \
  194. } \
  195. }
  196. struct ClassSettings {
  197. String className; // Following a line with [className] in the *.ini configuration file.
  198. List<KeywordEntry<PersistentImage>> colorImages;
  199. List<KeywordEntry<FixedPoint>> scalars;
  200. List<KeywordEntry<String>> strings;
  201. ClassSettings(const ReadableString &className)
  202. : className(className) {}
  203. bool keyExists(const ReadableString &key) {
  204. FOR_EACH_COLLECTION(this->, RETURN_TRUE_IF_SETTING_EXISTS)
  205. return false;
  206. }
  207. void setVariable(const ReadableString &key, const ReadableString &value, const ReadableString &fromPath) {
  208. if (this->keyExists(key)) { throwError(U"The property ", key, U" was defined multiple times in ", className, U"\n"); }
  209. DsrChar firstCharacter = value[0];
  210. if (firstCharacter == U'\"') {
  211. // Key = "text"
  212. this->strings.pushConstruct(key, string_unmangleQuote(value));
  213. } else {
  214. if (string_findFirst(value, U':') > -1) {
  215. // Key = File:Path
  216. // Key = WxH:Hexadecimals
  217. PersistentImage newImage;
  218. newImage.assignValue(value, fromPath);
  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 MediaMachine &machine, const ReadableString &styleSettings, const ReadableString &fromPath) : machine(machine) {
  250. this->settings.pushConstruct(U"default");
  251. config_parse_ini(styleSettings, [this, fromPath](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, fromPath);
  254. });
  255. }
  256. // Destructor
  257. virtual ~VisualThemeImpl() {}
  258. };
  259. static VisualTheme defaultTheme;
  260. VisualTheme theme_getDefault() {
  261. if (!(defaultTheme.get())) {
  262. defaultTheme = theme_createFromText(machine_create(defaultMediaMachineCode), defaultStyleSettings, file_getCurrentPath());
  263. }
  264. return defaultTheme;
  265. }
  266. VisualTheme theme_createFromText(const MediaMachine &machine, const ReadableString &styleSettings, const ReadableString &fromPath) {
  267. return std::make_shared<VisualThemeImpl>(machine, styleSettings, fromPath);
  268. }
  269. VisualTheme theme_createFromFile(const MediaMachine &machine, const ReadableString &styleFilename) {
  270. return theme_createFromText(machine, string_load(styleFilename), file_getRelativeParentFolder(styleFilename));
  271. }
  272. MediaMethod theme_getScalableImage(const VisualTheme &theme, const ReadableString &className) {
  273. if (!theme.get()) {
  274. throwError(U"theme_getScalableImage: Can't get scalable image from a non-existing theme!\n");
  275. }
  276. int classIndex = theme->getClassIndex(className);
  277. if (classIndex == -1) {
  278. throwError(U"theme_getScalableImage: Can't find any style class named ", className, U" in the given theme!\n");
  279. }
  280. // Try to get the method's name from the component's class settings,
  281. // and fall back on the class name itself if not found in neither the class settings nor the common default settings.
  282. String methodName;
  283. if (!theme->settings[classIndex].getString(methodName, U"method")) {
  284. if (!theme->settings[0].getString(methodName, U"method")) {
  285. throwError(U"The property \"method\" could not be found from the style class ", className, U", nor in the default settings!\n");
  286. }
  287. }
  288. return machine_getMethod(theme->machine, methodName, classIndex);
  289. }
  290. static bool assignMediaMachineArguments(ClassSettings settings, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
  291. // Search for argumentName in colorImages.
  292. for (int64_t i = 0; i < settings.colorImages.length(); i++) {
  293. if (string_caseInsensitiveMatch(settings.colorImages[i].key, argumentName)) {
  294. machine_setInputByIndex(machine, methodIndex, inputIndex, settings.colorImages[i].value.value);
  295. return true;
  296. }
  297. }
  298. // Search for argumentName in scalars.
  299. for (int64_t i = 0; i < settings.scalars.length(); i++) {
  300. if (string_caseInsensitiveMatch(settings.scalars[i].key, argumentName)) {
  301. machine_setInputByIndex(machine, methodIndex, inputIndex, settings.scalars[i].value);
  302. return true;
  303. }
  304. }
  305. return false;
  306. }
  307. bool theme_assignMediaMachineArguments(const VisualTheme &theme, int32_t contextIndex, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
  308. if (!theme.get()) { return false; }
  309. // Check in the context first.
  310. if (contextIndex > 0 && assignMediaMachineArguments(theme->settings[contextIndex], machine, methodIndex, inputIndex, argumentName)) {
  311. return true;
  312. } else {
  313. // If not found in the context, check in the default settings.
  314. return assignMediaMachineArguments(theme->settings[0], machine, methodIndex, inputIndex, argumentName);
  315. }
  316. }
  317. }