VisualTheme.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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. # Drawing a rounded rectangle to the alpha channel and a smaller rectangle reduced by the border argument for RGB channels.
  37. # This method for drawing edges works with alpha filtering enabled.
  38. BEGIN: generate_rounded_rectangle
  39. # Dimensions of the result image.
  40. INPUT: FixedPoint, width
  41. INPUT: FixedPoint, height
  42. # The subtracted offset from the radius to create a border on certain channels.
  43. INPUT: FixedPoint, border
  44. # The whole pixel radius from center points to the end of the image.
  45. INPUT: FixedPoint, rounding
  46. # Create the result image.
  47. OUTPUT: ImageU8, resultImage
  48. CREATE: resultImage, width, height
  49. # Limit outer radius to half of the image's minimum dimension.
  50. MIN: radius<FixedPoint>, width, height
  51. MUL: radius, radius, 0.5
  52. MIN: radius, radius, rounding
  53. ROUND: radius, radius
  54. # Place the inner radius for drawing.
  55. SUB: innerRadius<FixedPoint>, rounding, border
  56. # Use +- 0.5 pixel offsets for fake anti-aliasing.
  57. ADD: radiusOut<FixedPoint>, innerRadius, 0.5
  58. ADD: radiusIn<FixedPoint>, innerRadius, -0.5
  59. # Calculate dimensions for drawing.
  60. SUB: w2<FixedPoint>, width, radius
  61. SUB: w3<FixedPoint>, w2, radius
  62. SUB: w4<FixedPoint>, width, border
  63. SUB: w4, w4, border
  64. SUB: h2<FixedPoint>, height, radius
  65. SUB: h3<FixedPoint>, h2, radius
  66. SUB: r2<FixedPoint>, radius, border
  67. # Draw.
  68. FADE_REGION_RADIAL: resultImage, 0, 0, radius, radius, radius, radius, radiusIn, 255, radiusOut, 0
  69. FADE_REGION_RADIAL: resultImage, w2, 0, radius, radius, 0, radius, radiusIn, 255, radiusOut, 0
  70. FADE_REGION_RADIAL: resultImage, 0, h2, radius, radius, radius, 0, radiusIn, 255, radiusOut, 0
  71. FADE_REGION_RADIAL: resultImage, w2, h2, radius, radius, 0, 0, radiusIn, 255, radiusOut, 0
  72. RECTANGLE: resultImage, radius, border, w3, r2, 255
  73. RECTANGLE: resultImage, radius, h2, w3, r2, 255
  74. RECTANGLE: resultImage, border, radius, w4, h3, 255
  75. END:
  76. # Can be call directly to draw a component, or internally to add more effects.
  77. # Black edges are created by default initializing the background to zeroes and drawing the inside smaller.
  78. # This method for drawing edges does not work if the resulting colorImage is drawn with alpha filtering, because setting alpha to zero means transparent.
  79. BEGIN: HardRectangle
  80. INPUT: FixedPoint, width
  81. INPUT: FixedPoint, height
  82. INPUT: FixedPoint, red
  83. INPUT: FixedPoint, green
  84. INPUT: FixedPoint, blue
  85. INPUT: FixedPoint, border
  86. OUTPUT: ImageRgbaU8, colorImage
  87. CREATE: colorImage, width, height
  88. ADD: b2<FixedPoint>, border, border
  89. SUB: w2<FixedPoint>, width, b2
  90. SUB: h2<FixedPoint>, height, b2
  91. RECTANGLE: colorImage, border, border, w2, h2, red, green, blue, 255
  92. END:
  93. BEGIN: generate_rounded_button
  94. INPUT: FixedPoint, width
  95. INPUT: FixedPoint, height
  96. INPUT: FixedPoint, red
  97. INPUT: FixedPoint, green
  98. INPUT: FixedPoint, blue
  99. INPUT: FixedPoint, pressed
  100. INPUT: FixedPoint, border
  101. INPUT: FixedPoint, rounding
  102. OUTPUT: ImageRgbaU8, resultImage
  103. # Scale by 2 / 255 so that 127.5 represents full intensity in patternImage.
  104. MUL: normRed<FixedPoint>, red, 0.007843138
  105. MUL: normGreen<FixedPoint>, green, 0.007843138
  106. MUL: normBlue<FixedPoint>, blue, 0.007843138
  107. CREATE: patternImage<ImageU8>, width, height
  108. MUL: pressDarknessHigh<FixedPoint>, pressed, 80
  109. MUL: pressDarknessLow<FixedPoint>, pressed, 10
  110. SUB: highLuma<FixedPoint>, 150, pressDarknessHigh
  111. SUB: lowLuma<FixedPoint>, 100, pressDarknessLow
  112. FADE_LINEAR: patternImage, 0, 0, highLuma, 0, height, lowLuma
  113. CALL: generate_rounded_rectangle, lumaImage<ImageU8>, width, height, border, rounding
  114. MUL: lumaImage, lumaImage, patternImage, 0.003921569
  115. CALL: generate_rounded_rectangle, visImage<ImageU8>, width, height, 0, rounding
  116. MUL: redImage<ImageU8>, lumaImage, normRed
  117. MUL: greenImage<ImageU8>, lumaImage, normGreen
  118. MUL: blueImage<ImageU8>, lumaImage, normBlue
  119. PACK_RGBA: resultImage, redImage, greenImage, blueImage, visImage
  120. END:
  121. BEGIN: Button
  122. INPUT: FixedPoint, width
  123. INPUT: FixedPoint, height
  124. INPUT: FixedPoint, red
  125. INPUT: FixedPoint, green
  126. INPUT: FixedPoint, blue
  127. INPUT: FixedPoint, pressed
  128. INPUT: FixedPoint, border
  129. INPUT: FixedPoint, rounding
  130. OUTPUT: ImageRgbaU8, colorImage
  131. CALL: generate_rounded_button, colorImage, width, height, red, green, blue, pressed, border, rounding
  132. END:
  133. BEGIN: VerticalScrollList
  134. INPUT: FixedPoint, width
  135. INPUT: FixedPoint, height
  136. INPUT: FixedPoint, red
  137. INPUT: FixedPoint, green
  138. INPUT: FixedPoint, blue
  139. OUTPUT: ImageRgbaU8, colorImage
  140. CREATE: visImage<ImageU8>, width, height
  141. CREATE: lumaImage<ImageU8>, width, height
  142. FADE_LINEAR: visImage, 0, 0, 128, width, 0, 0
  143. PACK_RGBA: colorImage, 0, 0, 0, visImage
  144. END:
  145. BEGIN: HorizontalScrollList
  146. INPUT: FixedPoint, width
  147. INPUT: FixedPoint, height
  148. INPUT: FixedPoint, red
  149. INPUT: FixedPoint, green
  150. INPUT: FixedPoint, blue
  151. OUTPUT: ImageRgbaU8, colorImage
  152. CREATE: visImage<ImageU8>, width, height
  153. CREATE: lumaImage<ImageU8>, width, height
  154. FADE_LINEAR: visImage, 0, 0, 128, 0, height, 0
  155. PACK_RGBA: colorImage, 0, 0, 0, visImage
  156. END:
  157. BEGIN: TextBox
  158. INPUT: FixedPoint, width
  159. INPUT: FixedPoint, height
  160. INPUT: FixedPoint, red
  161. INPUT: FixedPoint, green
  162. INPUT: FixedPoint, blue
  163. INPUT: FixedPoint, border
  164. INPUT: FixedPoint, focused
  165. OUTPUT: ImageRgbaU8, colorImage
  166. ADD: intensity<FixedPoint>, 4, focused
  167. MUL: intensity, intensity, 0.2
  168. MUL: red, red, intensity
  169. MUL: green, green, intensity
  170. MUL: blue, blue, intensity
  171. CALL: HardRectangle, colorImage, width, height, red, green, blue, border
  172. END:
  173. )QUOTE";
  174. // Using *.ini files for storing style settings as a simple start.
  175. // A more advanced system will be used later.
  176. static const ReadableString defaultStyleSettings =
  177. UR"QUOTE(
  178. border = 2
  179. method = "Button"
  180. ; Fall back on the Button method if a component's class could not be recognized.
  181. [Button]
  182. rounding = 12
  183. [ListBox]
  184. method = "HardRectangle"
  185. [TextBox]
  186. method = "TextBox"
  187. [VerticalScrollKnob]
  188. rounding = 8
  189. [HorizontalScrollKnob]
  190. rounding = 8
  191. [VerticalScrollList]
  192. method = "VerticalScrollList"
  193. [HorizontalScrollList]
  194. method = "HorizontalScrollList"
  195. [ScrollUp]
  196. rounding = 5
  197. [ScrollDown]
  198. rounding = 5
  199. [ScrollLeft]
  200. rounding = 5
  201. [ScrollRight]
  202. rounding = 5
  203. [Panel]
  204. border = 1
  205. method = "HardRectangle"
  206. )QUOTE";
  207. template <typename V>
  208. struct KeywordEntry {
  209. String key;
  210. V value;
  211. KeywordEntry(const ReadableString &key, const V &value)
  212. : key(key), value(value) {}
  213. };
  214. #define FOR_EACH_COLLECTION(LOCATION, MACRO_NAME) \
  215. MACRO_NAME(LOCATION colorImages) \
  216. MACRO_NAME(LOCATION scalars) \
  217. MACRO_NAME(LOCATION strings)
  218. #define RETURN_TRUE_IF_SETTING_EXISTS(COLLECTION) \
  219. for (int64_t i = 0; i < COLLECTION.length(); i++) { \
  220. if (string_caseInsensitiveMatch(COLLECTION[i].key, key)) { \
  221. return true; \
  222. } \
  223. }
  224. struct ClassSettings {
  225. String className; // Following a line with [className] in the *.ini configuration file.
  226. List<KeywordEntry<PersistentImage>> colorImages;
  227. List<KeywordEntry<FixedPoint>> scalars;
  228. List<KeywordEntry<String>> strings;
  229. ClassSettings(const ReadableString &className)
  230. : className(className) {}
  231. bool keyExists(const ReadableString &key) {
  232. FOR_EACH_COLLECTION(this->, RETURN_TRUE_IF_SETTING_EXISTS)
  233. return false;
  234. }
  235. void setVariable(const ReadableString &key, const ReadableString &value, const ReadableString &fromPath) {
  236. if (this->keyExists(key)) { throwError(U"The property ", key, U" was defined multiple times in ", className, U"\n"); }
  237. DsrChar firstCharacter = value[0];
  238. if (firstCharacter == U'\"') {
  239. // Key = "text"
  240. this->strings.pushConstruct(key, string_unmangleQuote(value));
  241. } else {
  242. if (string_findFirst(value, U':') > -1) {
  243. // Key = File:Path
  244. // Key = WxH:Hexadecimals
  245. PersistentImage newImage;
  246. newImage.assignValue(value, fromPath);
  247. this->colorImages.pushConstruct(key, newImage);
  248. } else {
  249. // Key = Integer
  250. // Key = Integer.Decimals
  251. this->scalars.pushConstruct(key, FixedPoint::fromText(value));
  252. }
  253. }
  254. }
  255. // Post-condition: Returns true iff the key was found for the expected type.
  256. // Side-effect: Writes the value of the found key iff found.
  257. bool getString(String &target, const ReadableString &key) {
  258. for (int64_t i = 0; i < this->strings.length(); i++) {
  259. if (string_caseInsensitiveMatch(this->strings[i].key, key)) {
  260. target = this->strings[i].value;
  261. return true;
  262. }
  263. }
  264. return false;
  265. }
  266. };
  267. // 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.
  268. class VisualThemeImpl {
  269. public:
  270. MediaMachine machine;
  271. List<ClassSettings> settings;
  272. int32_t getClassIndex(const ReadableString& className) {
  273. for (int64_t i = 0; i < this->settings.length(); i++) { if (string_caseInsensitiveMatch(this->settings[i].className, className)) { return i; } }
  274. settings.pushConstruct(className);
  275. return settings.length() - 1;
  276. }
  277. VisualThemeImpl(const MediaMachine &machine, const ReadableString &styleSettings, const ReadableString &fromPath) : machine(machine) {
  278. this->settings.pushConstruct(U"default");
  279. config_parse_ini(styleSettings, [this, fromPath](const ReadableString& block, const ReadableString& key, const ReadableString& value) {
  280. int32_t classIndex = (string_length(block) == 0) ? 0 : this->getClassIndex(block);
  281. this->settings[classIndex].setVariable(key, value, fromPath);
  282. });
  283. }
  284. // Destructor
  285. virtual ~VisualThemeImpl() {}
  286. };
  287. static VisualTheme defaultTheme;
  288. VisualTheme theme_getDefault() {
  289. if (!(defaultTheme.get())) {
  290. defaultTheme = theme_createFromText(machine_create(defaultMediaMachineCode), defaultStyleSettings, file_getCurrentPath());
  291. }
  292. return defaultTheme;
  293. }
  294. VisualTheme theme_createFromText(const MediaMachine &machine, const ReadableString &styleSettings, const ReadableString &fromPath) {
  295. return std::make_shared<VisualThemeImpl>(machine, styleSettings, fromPath);
  296. }
  297. VisualTheme theme_createFromFile(const MediaMachine &machine, const ReadableString &styleFilename) {
  298. return theme_createFromText(machine, string_load(styleFilename), file_getRelativeParentFolder(styleFilename));
  299. }
  300. MediaMethod theme_getScalableImage(const VisualTheme &theme, const ReadableString &className) {
  301. if (!theme.get()) {
  302. throwError(U"theme_getScalableImage: Can't get scalable image from a non-existing theme!\n");
  303. }
  304. int classIndex = theme->getClassIndex(className);
  305. if (classIndex == -1) {
  306. throwError(U"theme_getScalableImage: Can't find any style class named ", className, U" in the given theme!\n");
  307. }
  308. // Try to get the method's name from the component's class settings,
  309. // and fall back on the class name itself if not found in neither the class settings nor the common default settings.
  310. String methodName;
  311. if (!theme->settings[classIndex].getString(methodName, U"method")) {
  312. if (!theme->settings[0].getString(methodName, U"method")) {
  313. throwError(U"The property \"method\" could not be found from the style class ", className, U", nor in the default settings!\n");
  314. }
  315. }
  316. return machine_getMethod(theme->machine, methodName, classIndex);
  317. }
  318. static bool assignMediaMachineArguments(ClassSettings settings, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
  319. // Search for argumentName in colorImages.
  320. for (int64_t i = 0; i < settings.colorImages.length(); i++) {
  321. if (string_caseInsensitiveMatch(settings.colorImages[i].key, argumentName)) {
  322. machine_setInputByIndex(machine, methodIndex, inputIndex, settings.colorImages[i].value.value);
  323. return true;
  324. }
  325. }
  326. // Search for argumentName in scalars.
  327. for (int64_t i = 0; i < settings.scalars.length(); i++) {
  328. if (string_caseInsensitiveMatch(settings.scalars[i].key, argumentName)) {
  329. machine_setInputByIndex(machine, methodIndex, inputIndex, settings.scalars[i].value);
  330. return true;
  331. }
  332. }
  333. // The media machine currently does not support strings.
  334. return false;
  335. }
  336. bool theme_assignMediaMachineArguments(const VisualTheme &theme, int32_t contextIndex, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
  337. if (!theme.get()) { return false; }
  338. // Check in the context first, and then in the default settings.
  339. return (contextIndex > 0 && assignMediaMachineArguments(theme->settings[contextIndex], machine, methodIndex, inputIndex, argumentName))
  340. || assignMediaMachineArguments(theme->settings[0], machine, methodIndex, inputIndex, argumentName);
  341. }
  342. }