VisualTheme.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. // zlib open source license
  2. //
  3. // Copyright (c) 2018 to 2023 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 <cstdint>
  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. MIN: innerRadius<FixedPoint>, radius, rounding
  56. SUB: innerRadius, innerRadius, border
  57. # Use +- 0.5 pixel offsets for fake anti-aliasing.
  58. ADD: radiusOut<FixedPoint>, innerRadius, 0.5
  59. ADD: radiusIn<FixedPoint>, innerRadius, -0.5
  60. # Calculate dimensions for drawing.
  61. SUB: w2<FixedPoint>, width, radius
  62. SUB: w3<FixedPoint>, w2, radius
  63. SUB: w4<FixedPoint>, width, border
  64. SUB: w4, w4, border
  65. SUB: h2<FixedPoint>, height, radius
  66. SUB: h3<FixedPoint>, h2, radius
  67. SUB: r2<FixedPoint>, radius, border
  68. # Draw.
  69. FADE_REGION_RADIAL: resultImage, 0, 0, radius, radius, radius, radius, radiusIn, 255, radiusOut, 0
  70. FADE_REGION_RADIAL: resultImage, w2, 0, radius, radius, 0, radius, radiusIn, 255, radiusOut, 0
  71. FADE_REGION_RADIAL: resultImage, 0, h2, radius, radius, radius, 0, radiusIn, 255, radiusOut, 0
  72. FADE_REGION_RADIAL: resultImage, w2, h2, radius, radius, 0, 0, radiusIn, 255, radiusOut, 0
  73. RECTANGLE: resultImage, radius, border, w3, r2, 255
  74. RECTANGLE: resultImage, radius, h2, w3, r2, 255
  75. RECTANGLE: resultImage, border, radius, w4, h3, 255
  76. END:
  77. # Can be call directly to draw a component, or internally to add more effects.
  78. # Black edges are created by default initializing the background to zeroes and drawing the inside smaller.
  79. # This method for drawing edges does not work if the resulting colorImage is drawn with alpha filtering, because setting alpha to zero means transparent.
  80. BEGIN: HardRectangle
  81. INPUT: FixedPoint, width
  82. INPUT: FixedPoint, height
  83. INPUT: FixedPoint, red
  84. INPUT: FixedPoint, green
  85. INPUT: FixedPoint, blue
  86. INPUT: FixedPoint, border
  87. OUTPUT: ImageRgbaU8, colorImage
  88. CREATE: colorImage, width, height
  89. ADD: b2<FixedPoint>, border, border
  90. SUB: w2<FixedPoint>, width, b2
  91. SUB: h2<FixedPoint>, height, b2
  92. RECTANGLE: colorImage, border, border, w2, h2, red, green, blue, 255
  93. END:
  94. BEGIN: generate_rounded_button
  95. INPUT: FixedPoint, width
  96. INPUT: FixedPoint, height
  97. INPUT: FixedPoint, red
  98. INPUT: FixedPoint, green
  99. INPUT: FixedPoint, blue
  100. INPUT: FixedPoint, pressed
  101. INPUT: FixedPoint, border
  102. INPUT: FixedPoint, rounding
  103. OUTPUT: ImageRgbaU8, resultImage
  104. # Scale by 2 / 255 so that 127.5 represents full intensity in patternImage.
  105. MUL: normRed<FixedPoint>, red, 0.007843138
  106. MUL: normGreen<FixedPoint>, green, 0.007843138
  107. MUL: normBlue<FixedPoint>, blue, 0.007843138
  108. CREATE: patternImage<ImageU8>, width, height
  109. MUL: pressDarknessHigh<FixedPoint>, pressed, 80
  110. MUL: pressDarknessLow<FixedPoint>, pressed, 10
  111. SUB: highLuma<FixedPoint>, 150, pressDarknessHigh
  112. SUB: lowLuma<FixedPoint>, 100, pressDarknessLow
  113. FADE_LINEAR: patternImage, 0, 0, highLuma, 0, height, lowLuma
  114. CALL: generate_rounded_rectangle, lumaImage<ImageU8>, width, height, border, rounding
  115. MUL: lumaImage, lumaImage, patternImage, 0.003921569
  116. CALL: generate_rounded_rectangle, visImage<ImageU8>, width, height, 0, rounding
  117. MUL: redImage<ImageU8>, lumaImage, normRed
  118. MUL: greenImage<ImageU8>, lumaImage, normGreen
  119. MUL: blueImage<ImageU8>, lumaImage, normBlue
  120. PACK_RGBA: resultImage, redImage, greenImage, blueImage, visImage
  121. END:
  122. BEGIN: Button
  123. INPUT: FixedPoint, width
  124. INPUT: FixedPoint, height
  125. INPUT: FixedPoint, red
  126. INPUT: FixedPoint, green
  127. INPUT: FixedPoint, blue
  128. INPUT: FixedPoint, pressed
  129. INPUT: FixedPoint, border
  130. INPUT: FixedPoint, rounding
  131. OUTPUT: ImageRgbaU8, colorImage
  132. CALL: generate_rounded_button, colorImage, width, height, red, green, blue, pressed, border, rounding
  133. END:
  134. BEGIN: VerticalScrollList
  135. INPUT: FixedPoint, width
  136. INPUT: FixedPoint, height
  137. INPUT: FixedPoint, red
  138. INPUT: FixedPoint, green
  139. INPUT: FixedPoint, blue
  140. OUTPUT: ImageRgbaU8, colorImage
  141. CREATE: visImage<ImageU8>, width, height
  142. CREATE: lumaImage<ImageU8>, width, height
  143. FADE_LINEAR: visImage, 0, 0, 128, width, 0, 0
  144. PACK_RGBA: colorImage, 0, 0, 0, visImage
  145. END:
  146. BEGIN: HorizontalScrollList
  147. INPUT: FixedPoint, width
  148. INPUT: FixedPoint, height
  149. INPUT: FixedPoint, red
  150. INPUT: FixedPoint, green
  151. INPUT: FixedPoint, blue
  152. OUTPUT: ImageRgbaU8, colorImage
  153. CREATE: visImage<ImageU8>, width, height
  154. CREATE: lumaImage<ImageU8>, width, height
  155. FADE_LINEAR: visImage, 0, 0, 128, 0, height, 0
  156. PACK_RGBA: colorImage, 0, 0, 0, visImage
  157. END:
  158. BEGIN: TextBox
  159. INPUT: FixedPoint, width
  160. INPUT: FixedPoint, height
  161. INPUT: FixedPoint, red
  162. INPUT: FixedPoint, green
  163. INPUT: FixedPoint, blue
  164. INPUT: FixedPoint, border
  165. INPUT: FixedPoint, focused
  166. OUTPUT: ImageRgbaU8, colorImage
  167. ADD: intensity<FixedPoint>, 4, focused
  168. MUL: intensity, intensity, 0.2
  169. MUL: red, red, intensity
  170. MUL: green, green, intensity
  171. MUL: blue, blue, intensity
  172. CALL: HardRectangle, colorImage, width, height, red, green, blue, border
  173. END:
  174. )QUOTE";
  175. // Using *.ini files for storing style settings as a simple start.
  176. // A more advanced system will be used later.
  177. static const ReadableString defaultStyleSettings =
  178. UR"QUOTE(
  179. border = 2
  180. method = "Button"
  181. ; Fall back on the Button method if a component's class could not be recognized.
  182. [Button]
  183. rounding = 12
  184. filter = 1
  185. method = "Button"
  186. [ListBox]
  187. method = "HardRectangle"
  188. [TextBox]
  189. method = "TextBox"
  190. [VerticalScrollKnob]
  191. rounding = 8
  192. [HorizontalScrollKnob]
  193. rounding = 8
  194. [VerticalScrollList]
  195. method = "VerticalScrollList"
  196. [HorizontalScrollList]
  197. method = "HorizontalScrollList"
  198. [ScrollUp]
  199. rounding = 5
  200. [ScrollDown]
  201. rounding = 5
  202. [ScrollLeft]
  203. rounding = 5
  204. [ScrollRight]
  205. rounding = 5
  206. [Panel]
  207. border = 1
  208. method = "HardRectangle"
  209. [Toolbar]
  210. border = 1
  211. method = "HardRectangle"
  212. [MenuTop]
  213. border = 1
  214. method = "HardRectangle"
  215. [MenuSub]
  216. border = 1
  217. method = "HardRectangle"
  218. [MenuList]
  219. border = 1
  220. method = "HardRectangle"
  221. )QUOTE";
  222. template <typename V>
  223. struct KeywordEntry {
  224. String key;
  225. V value;
  226. KeywordEntry(const ReadableString &key, const V &value)
  227. : key(key), value(value) {}
  228. };
  229. #define FOR_EACH_COLLECTION(LOCATION, MACRO_NAME) \
  230. MACRO_NAME(LOCATION colorImages) \
  231. MACRO_NAME(LOCATION scalars) \
  232. MACRO_NAME(LOCATION strings)
  233. #define RETURN_TRUE_IF_SETTING_EXISTS(COLLECTION) \
  234. for (int i = 0; i < COLLECTION.length(); i++) { \
  235. if (string_caseInsensitiveMatch(COLLECTION[i].key, key)) { \
  236. return true; \
  237. } \
  238. }
  239. struct ClassSettings {
  240. String className; // Following a line with [className] in the *.ini configuration file.
  241. List<KeywordEntry<PersistentImage>> colorImages;
  242. List<KeywordEntry<FixedPoint>> scalars;
  243. List<KeywordEntry<String>> strings;
  244. ClassSettings(const ReadableString &className)
  245. : className(className) {}
  246. bool keyExists(const ReadableString &key) {
  247. FOR_EACH_COLLECTION(this->, RETURN_TRUE_IF_SETTING_EXISTS)
  248. return false;
  249. }
  250. void setVariable(const ReadableString &key, const ReadableString &value, const ReadableString &fromPath) {
  251. if (this->keyExists(key)) { throwError(U"The property ", key, U" was defined multiple times in ", className, U"\n"); }
  252. DsrChar firstCharacter = value[0];
  253. if (firstCharacter == U'\"') {
  254. // Key = "text"
  255. this->strings.pushConstruct(key, string_unmangleQuote(value));
  256. } else {
  257. if (string_findFirst(value, U':') > -1) {
  258. // Key = File:Path
  259. // Key = WxH:Hexadecimals
  260. PersistentImage newImage;
  261. newImage.assignValue(value, fromPath);
  262. this->colorImages.pushConstruct(key, newImage);
  263. } else {
  264. // Key = Integer
  265. // Key = Integer.Decimals
  266. this->scalars.pushConstruct(key, FixedPoint::fromText(value));
  267. }
  268. }
  269. }
  270. // Post-condition: Returns true iff the key was found for the expected type.
  271. // Side-effect: Writes the value of the found key iff found.
  272. bool getString(String &target, const ReadableString &key) {
  273. for (int i = 0; i < this->strings.length(); i++) {
  274. if (string_caseInsensitiveMatch(this->strings[i].key, key)) {
  275. target = this->strings[i].value;
  276. return true;
  277. }
  278. }
  279. return false;
  280. }
  281. // Post-condition: Returns true iff the key was found for the expected type.
  282. // Side-effect: Writes the value of the found key iff found.
  283. bool getImage(PersistentImage &target, const ReadableString &key) {
  284. for (int i = 0; i < this->colorImages.length(); i++) {
  285. if (string_caseInsensitiveMatch(this->colorImages[i].key, key)) {
  286. target = this->colorImages[i].value;
  287. return true;
  288. }
  289. }
  290. return false;
  291. }
  292. // Post-condition: Returns true iff the key was found for the expected type.
  293. // Side-effect: Writes the value of the found key iff found.
  294. bool getScalar(FixedPoint &target, const ReadableString &key) {
  295. for (int i = 0; i < this->scalars.length(); i++) {
  296. if (string_caseInsensitiveMatch(this->scalars[i].key, key)) {
  297. target = this->scalars[i].value;
  298. return true;
  299. }
  300. }
  301. return false;
  302. }
  303. };
  304. // 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.
  305. class VisualThemeImpl {
  306. public:
  307. MediaMachine machine;
  308. List<ClassSettings> settings;
  309. int getClassIndex(const ReadableString& className) {
  310. for (int i = 0; i < this->settings.length(); i++) { if (string_caseInsensitiveMatch(this->settings[i].className, className)) { return i; } }
  311. return settings.pushConstructGetIndex(className);
  312. }
  313. VisualThemeImpl(const MediaMachine &machine, const ReadableString &styleSettings, const ReadableString &fromPath) : machine(machine) {
  314. this->settings.pushConstruct(U"default");
  315. config_parse_ini(styleSettings, [this, fromPath](const ReadableString& block, const ReadableString& key, const ReadableString& value) {
  316. int classIndex = (string_length(block) == 0) ? 0 : this->getClassIndex(block);
  317. this->settings[classIndex].setVariable(key, value, fromPath);
  318. });
  319. }
  320. // Destructor
  321. virtual ~VisualThemeImpl() {}
  322. };
  323. static VisualTheme defaultTheme;
  324. VisualTheme theme_getDefault() {
  325. if (!(defaultTheme.getUnsafe())) {
  326. defaultTheme = theme_createFromText(machine_create(defaultMediaMachineCode), defaultStyleSettings, file_getCurrentPath());
  327. }
  328. return defaultTheme;
  329. }
  330. VisualTheme theme_createFromText(const MediaMachine &machine, const ReadableString &styleSettings, const ReadableString &fromPath) {
  331. return handle_create<VisualThemeImpl>(machine, styleSettings, fromPath).setName("Visual Theme");
  332. }
  333. VisualTheme theme_createFromFile(const MediaMachine &machine, const ReadableString &styleFilename) {
  334. return theme_createFromText(machine, string_load(styleFilename), file_getRelativeParentFolder(styleFilename)).setName("Visual Theme");
  335. }
  336. bool theme_exists(const VisualTheme &theme) {
  337. return theme.isNotNull();
  338. }
  339. int theme_getClassIndex(const VisualTheme &theme, const ReadableString &className) {
  340. if (!theme_exists(theme)) {
  341. return -1;
  342. } else if (string_length(className) == 0) {
  343. return 0;
  344. } else {
  345. int classIndex = theme->getClassIndex(className);
  346. return (classIndex == -1) ? 0 : classIndex;
  347. }
  348. }
  349. bool theme_class_exists(const VisualTheme &theme, const ReadableString &className) {
  350. return theme_getClassIndex(theme, className) > 0;
  351. }
  352. String theme_selectClass(const VisualTheme &theme, const ReadableString &suggestedClassName, const ReadableString &fallbackClassName) {
  353. return theme_class_exists(theme, suggestedClassName) ? suggestedClassName : fallbackClassName;
  354. }
  355. OrderedImageRgbaU8 theme_getImage(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName) {
  356. if (!theme.getUnsafe()) {
  357. return OrderedImageRgbaU8();
  358. }
  359. int classIndex = theme->getClassIndex(className);
  360. PersistentImage result;
  361. if ((classIndex != -1 && theme->settings[classIndex].getImage(result, settingName))
  362. || (theme->settings[0].getImage(result, settingName))) {
  363. // If the class existed and it contained the setting or the setting could be found in the default class then return it.
  364. return result.value;
  365. } else {
  366. return OrderedImageRgbaU8();
  367. }
  368. }
  369. FixedPoint theme_getFixedPoint(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName, const FixedPoint &defaultValue) {
  370. if (!theme.getUnsafe()) {
  371. return defaultValue;
  372. }
  373. int classIndex = theme->getClassIndex(className);
  374. FixedPoint result;
  375. if ((classIndex != -1 && theme->settings[classIndex].getScalar(result, settingName))
  376. || (theme->settings[0].getScalar(result, settingName))) {
  377. // If the class existed and it contained the setting or the setting could be found in the default class then return it.
  378. return result;
  379. } else {
  380. return defaultValue;
  381. }
  382. }
  383. int theme_getInteger(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName, const int &defaultValue) {
  384. return fixedPoint_round(theme_getFixedPoint(theme, className, settingName, FixedPoint::fromWhole(defaultValue)));
  385. }
  386. ReadableString theme_getString(const VisualTheme &theme, const ReadableString &className, const ReadableString &settingName, const ReadableString &defaultValue) {
  387. if (!theme.getUnsafe()) {
  388. return defaultValue;
  389. }
  390. int classIndex = theme->getClassIndex(className);
  391. String result;
  392. if ((classIndex != -1 && theme->settings[classIndex].getString(result, settingName))
  393. || (theme->settings[0].getString(result, settingName))) {
  394. // If the class existed and it contained the setting or the setting could be found in the default class then return it.
  395. return result;
  396. } else {
  397. return defaultValue;
  398. }
  399. }
  400. MediaMethod theme_getScalableImage(const VisualTheme &theme, const ReadableString &className) {
  401. if (!theme.getUnsafe()) {
  402. throwError(U"theme_getScalableImage: Can't get scalable image of class ", className, U" from a non-existing theme!\n");
  403. }
  404. int classIndex = theme->getClassIndex(className);
  405. String methodName;
  406. if ((classIndex != -1 && theme->settings[classIndex].getString(methodName, U"method"))
  407. || (theme->settings[0].getString(methodName, U"method"))) {
  408. // If the class existed and it contained the setting or the setting could be found in the default class then return it.
  409. return machine_getMethod(theme->machine, methodName, theme->getClassIndex(className));
  410. } else {
  411. throwError(U"theme_getScalableImage: Can't get scalable image of class ", className, U" because the setting did not exist in neither the class nor the default settings!\n");
  412. return MediaMethod();
  413. }
  414. }
  415. static bool assignMediaMachineArguments(ClassSettings settings, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
  416. // Search for argumentName in colorImages.
  417. for (int i = 0; i < settings.colorImages.length(); i++) {
  418. if (string_caseInsensitiveMatch(settings.colorImages[i].key, argumentName)) {
  419. machine_setInputByIndex(machine, methodIndex, inputIndex, settings.colorImages[i].value.value);
  420. return true;
  421. }
  422. }
  423. // Search for argumentName in scalars.
  424. for (int i = 0; i < settings.scalars.length(); i++) {
  425. if (string_caseInsensitiveMatch(settings.scalars[i].key, argumentName)) {
  426. machine_setInputByIndex(machine, methodIndex, inputIndex, settings.scalars[i].value);
  427. return true;
  428. }
  429. }
  430. // The media machine currently does not support strings.
  431. return false;
  432. }
  433. bool theme_assignMediaMachineArguments(const VisualTheme &theme, int contextIndex, MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName) {
  434. if (!theme.getUnsafe()) { return false; }
  435. // Check in the context first, and then in the default settings.
  436. return (contextIndex > 0 && assignMediaMachineArguments(theme->settings[contextIndex], machine, methodIndex, inputIndex, argumentName))
  437. || assignMediaMachineArguments(theme->settings[0], machine, methodIndex, inputIndex, argumentName);
  438. }
  439. ComponentState theme_getStateListenerMask(const MediaMethod &scalableImage) {
  440. ComponentState result = 0;
  441. for (int inputIndex = 0; inputIndex < machine_getInputCount(scalableImage.machine, scalableImage.methodIndex); inputIndex++) {
  442. String upperInputName = string_upperCase(machine_getInputName(scalableImage.machine, scalableImage.methodIndex, inputIndex));
  443. if (string_match(upperInputName, U"FOCUSED")) {
  444. result |= componentState_focusDirect;
  445. } else if (string_match(upperInputName, U"HOVERED")) {
  446. result |= componentState_hoverDirect;
  447. }
  448. }
  449. return result;
  450. }
  451. }