VisualComponent.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. // zlib open source license
  2. //
  3. // Copyright (c) 2018 to 2019 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 "VisualComponent.h"
  25. #include "../image/internal/imageInternal.h"
  26. using namespace dsr;
  27. PERSISTENT_DEFINITION(VisualComponent)
  28. VisualComponent::VisualComponent() {}
  29. VisualComponent::~VisualComponent() {
  30. this->callback_destroyEvent();
  31. // Let the children know that the parent component no longer exists.
  32. for (int i = 0; i < this->getChildCount(); i++) {
  33. this->children[i]->parent = nullptr;
  34. }
  35. }
  36. IVector2D VisualComponent::getDesiredDimensions() {
  37. // Unless this virtual method is overridden, toolbars and such will try to give these dimensions to the component.
  38. return IVector2D(32, 32);
  39. }
  40. bool VisualComponent::isContainer() const {
  41. return true;
  42. }
  43. IRect VisualComponent::getLocation() {
  44. // If someone requested access to Left, Top, Right or Bottom, regionAccessed will be true
  45. if (this->regionAccessed) {
  46. // Now that a fixed location is requested, we need to recalculate the location from the flexible region based on parent dimensions
  47. this->updateLayout();
  48. this->regionAccessed = false;
  49. }
  50. return this->location;
  51. }
  52. void VisualComponent::setRegion(const FlexRegion &newRegion) {
  53. this->region = newRegion;
  54. }
  55. FlexRegion VisualComponent::getRegion() const {
  56. return this->region;
  57. }
  58. void VisualComponent::setVisible(bool visible) {
  59. this->visible.value = visible;
  60. }
  61. bool VisualComponent::getVisible() const {
  62. return this->visible.value;
  63. }
  64. void VisualComponent::setName(const String& newName) {
  65. this->name.value = newName;
  66. }
  67. String VisualComponent::getName() const {
  68. return this->name.value;
  69. }
  70. void VisualComponent::setIndex(int newIndex) {
  71. this->index.value = newIndex;
  72. }
  73. int VisualComponent::getIndex() const {
  74. return this->index.value;
  75. }
  76. void VisualComponent::setLocation(const IRect &newLocation) {
  77. IRect oldLocation = this->location;
  78. this->location = newLocation;
  79. if (oldLocation != newLocation) {
  80. this->updateLocationEvent(oldLocation, newLocation);
  81. }
  82. this->changedLocation(oldLocation, newLocation);
  83. }
  84. void VisualComponent::updateLayout() {
  85. this->setLocation(this->region.getNewLocation(this->givenSpace));
  86. }
  87. void VisualComponent::applyLayout(const IRect& givenSpace) {
  88. this->givenSpace = givenSpace;
  89. this->updateLayout();
  90. }
  91. void VisualComponent::updateLocationEvent(const IRect& oldLocation, const IRect& newLocation) {
  92. // Place each child component
  93. for (int i = 0; i < this->getChildCount(); i++) {
  94. this->children[i]->applyLayout(IRect(0, 0, newLocation.width(), newLocation.height()));
  95. }
  96. }
  97. // Check if any change requires the child layout to update.
  98. // Used to realign members of toolbars after a desired dimension changed.
  99. void VisualComponent::updateChildLocations() {
  100. if (this->childChanged) {
  101. this->updateLocationEvent(this->location, this->location);
  102. this->childChanged = false;
  103. }
  104. }
  105. // Offset may become non-zero when the origin is outside of targetImage from being clipped outside of the parent region
  106. void VisualComponent::draw(ImageRgbaU8& targetImage, const IVector2D& offset) {
  107. if (this->getVisible()) {
  108. this->updateChildLocations();
  109. IRect containerBound = this->getLocation() + offset;
  110. this->drawSelf(targetImage, containerBound);
  111. // Draw each child component
  112. for (int i = 0; i < this->getChildCount(); i++) {
  113. this->children[i]->drawClipped(targetImage, containerBound.upperLeft(), containerBound);
  114. }
  115. // Draw the overlays
  116. if (this->overlayComponent.get() != nullptr) {
  117. this->overlayComponent->drawOverlay(targetImage);
  118. }
  119. }
  120. }
  121. void VisualComponent::drawClipped(ImageRgbaU8 targetImage, const IVector2D& offset, const IRect& clipRegion) {
  122. IRect finalRegion = IRect::cut(clipRegion, IRect(0, 0, image_getWidth(targetImage), image_getHeight(targetImage)));
  123. if (finalRegion.hasArea()) {
  124. // TODO: Optimize allocation of sub-images
  125. ImageRgbaU8 target = image_getSubImage(targetImage, finalRegion);
  126. this->draw(target, offset - finalRegion.upperLeft());
  127. }
  128. }
  129. // A red rectangle is drawn as a placeholder if the class couldn't be found
  130. // TODO: Should the type name be remembered in the base class for serializing missing components?
  131. void VisualComponent::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
  132. draw_rectangle(targetImage, relativeLocation, ColorRgbaI32(200, 50, 50, 255));
  133. }
  134. void VisualComponent::drawOverlay(ImageRgbaU8& targetImage) {}
  135. // Manual use with the correct type
  136. void VisualComponent::addChildComponent(std::shared_ptr<VisualComponent> child) {
  137. if (!this->isContainer()) {
  138. throwError(U"Cannot attach a child to a non-container parent component!\n");
  139. } else if (child.get() == this) {
  140. throwError(U"Cannot attach a component to itself!\n");
  141. } else if (child->hasChild(this)) {
  142. throwError(U"Cannot attach to its own parent as a child component!\n");
  143. } else {
  144. // Remove from any previous parent
  145. child->detachFromParent();
  146. // Update layout based on the new parent size
  147. child->applyLayout(IRect(0, 0, this->location.width(), this->location.height()));
  148. // Connect to the new parent
  149. this->children.push(child);
  150. this->childChanged = true;
  151. child->parent = this;
  152. }
  153. }
  154. // Automatic insertion from loading
  155. bool VisualComponent::addChild(std::shared_ptr<Persistent> child) {
  156. // Try to cast from base class Persistent to derived class VisualComponent
  157. std::shared_ptr<VisualComponent> visualComponent = std::dynamic_pointer_cast<VisualComponent>(child);
  158. if (visualComponent.get() == nullptr) {
  159. return false; // Wrong type!
  160. } else {
  161. this->addChildComponent(visualComponent);
  162. return true; // Success!
  163. }
  164. }
  165. int VisualComponent::getChildCount() const {
  166. return this->children.length();
  167. }
  168. std::shared_ptr<Persistent> VisualComponent::getChild(int index) const {
  169. if (index >= 0 && index < this->children.length()) {
  170. return this->children[index];
  171. } else {
  172. return std::shared_ptr<Persistent>(); // Null handle for out of bound.
  173. }
  174. }
  175. void VisualComponent::detachFromParent() {
  176. // Check if there's a parent component
  177. VisualComponent *parent = this->parent;
  178. if (parent != nullptr) {
  179. parent->childChanged = true;
  180. // If the removed component is focused from the parent, then remove focus so that the parent is focused instead.
  181. if (parent->focusComponent.get() == this) {
  182. parent->focusComponent = std::shared_ptr<VisualComponent>();
  183. }
  184. // Iterate over all children in the parent component
  185. for (int i = 0; i < parent->getChildCount(); i++) {
  186. std::shared_ptr<VisualComponent> current = parent->children[i];
  187. if (current.get() == this) {
  188. current->parent = nullptr; // Assign null
  189. parent->children.remove(i);
  190. return;
  191. }
  192. }
  193. }
  194. }
  195. bool VisualComponent::hasChild(VisualComponent *child) const {
  196. for (int i = 0; i < this->getChildCount(); i++) {
  197. std::shared_ptr<VisualComponent> current = this->children[i];
  198. if (current.get() == child) {
  199. return true; // Found the component
  200. } else {
  201. if (current->hasChild(child)) {
  202. return true; // Found the component recursively
  203. }
  204. }
  205. }
  206. return false; // Could not find the component
  207. }
  208. bool VisualComponent::hasChild(std::shared_ptr<VisualComponent> child) const {
  209. return this->hasChild(child.get());
  210. }
  211. std::shared_ptr<VisualComponent> VisualComponent::findChildByName(ReadableString name) const {
  212. for (int i = 0; i < this->getChildCount(); i++) {
  213. std::shared_ptr<VisualComponent> current = this->children[i];
  214. if (string_match(current->getName(), name)) {
  215. return current; // Found the component
  216. } else {
  217. std::shared_ptr<VisualComponent> searchResult = current->findChildByName(name);
  218. if (searchResult.get() != nullptr) {
  219. return searchResult; // Found the component recursively
  220. }
  221. }
  222. }
  223. return std::shared_ptr<VisualComponent>(); // Could not find the component
  224. }
  225. std::shared_ptr<VisualComponent> VisualComponent::findChildByNameAndIndex(ReadableString name, int index) const {
  226. for (int i = 0; i < this->getChildCount(); i++) {
  227. std::shared_ptr<VisualComponent> current = this->children[i];
  228. if (string_match(current->getName(), name) && current->getIndex() == index) {
  229. return current; // Found the component
  230. } else {
  231. std::shared_ptr<VisualComponent> searchResult = current->findChildByNameAndIndex(name, index);
  232. if (searchResult.get() != nullptr) {
  233. return searchResult; // Found the component recursively
  234. }
  235. }
  236. }
  237. return std::shared_ptr<VisualComponent>(); // Could not find the component
  238. }
  239. bool VisualComponent::pointIsInside(const IVector2D& pixelPosition) {
  240. return pixelPosition.x > this->location.left() && pixelPosition.x < this->location.right()
  241. && pixelPosition.y > this->location.top() && pixelPosition.y < this->location.bottom();
  242. }
  243. // Non-recursive top-down search
  244. std::shared_ptr<VisualComponent> VisualComponent::getDirectChild(const IVector2D& pixelPosition, bool includeInvisible) {
  245. // Iterate child components in reverse drawing order
  246. for (int i = this->getChildCount() - 1; i >= 0; i--) {
  247. std::shared_ptr<VisualComponent> currentChild = this->children[i];
  248. // Check if the point is inside the child component
  249. if ((currentChild->getVisible() || includeInvisible) && currentChild->pointIsInside(pixelPosition)) {
  250. return currentChild;
  251. }
  252. }
  253. // Return nothing if the point missed all child components
  254. return std::shared_ptr<VisualComponent>();
  255. }
  256. // Recursive top-down search
  257. std::shared_ptr<VisualComponent> VisualComponent::getTopChild(const IVector2D& pixelPosition, bool includeInvisible) {
  258. // Iterate child components in reverse drawing order
  259. for (int i = this->getChildCount() - 1; i >= 0; i--) {
  260. std::shared_ptr<VisualComponent> currentChild = this->children[i];
  261. // Check if the point is inside the child component
  262. if ((currentChild->getVisible() || includeInvisible) && currentChild->pointIsInside(pixelPosition)) {
  263. // Check if a component inside the child component is even higher up
  264. std::shared_ptr<VisualComponent> subChild = currentChild->getTopChild(pixelPosition - this->getLocation().upperLeft(), includeInvisible);
  265. if (subChild.get() != nullptr) {
  266. return subChild;
  267. } else {
  268. return currentChild;
  269. }
  270. }
  271. }
  272. // Return nothing if the point missed all child components
  273. return std::shared_ptr<VisualComponent>();
  274. }
  275. void VisualComponent::sendMouseEvent(const MouseEvent& event) {
  276. // Update the layout if needed
  277. this->updateChildLocations();
  278. // Convert to local coordinates recursively
  279. MouseEvent localEvent = event - this->getLocation().upperLeft();
  280. std::shared_ptr<VisualComponent> childComponent;
  281. // Grab a component on mouse down
  282. if (event.mouseEventType == MouseEventType::MouseDown) {
  283. childComponent = this->dragComponent = this->focusComponent = this->getDirectChild(localEvent.position, false);
  284. this->holdCount++;
  285. }
  286. if (this->holdCount > 0) {
  287. // If we're grabbing a component, keep sending events to it
  288. childComponent = this->dragComponent;
  289. } else if (this->getVisible() && this->pointIsInside(event.position)) {
  290. // If we're not grabbing a component, see if we can send the action to another component
  291. childComponent = this->getDirectChild(localEvent.position, false);
  292. }
  293. // Send the signal to a child component or itself
  294. if (childComponent.get() != nullptr) {
  295. childComponent->sendMouseEvent(localEvent);
  296. } else {
  297. this->receiveMouseEvent(event);
  298. }
  299. // Release a component on mouse up
  300. if (event.mouseEventType == MouseEventType::MouseUp) {
  301. this->holdCount--;
  302. if (this->holdCount <= 0) {
  303. this->dragComponent = std::shared_ptr<VisualComponent>(); // Abort drag
  304. this->holdCount = 0;
  305. }
  306. }
  307. }
  308. void VisualComponent::receiveMouseEvent(const MouseEvent& event) {
  309. if (event.mouseEventType == MouseEventType::MouseDown) {
  310. this->callback_mouseDownEvent(event);
  311. } else if (event.mouseEventType == MouseEventType::MouseUp) {
  312. this->callback_mouseUpEvent(event);
  313. } else if (event.mouseEventType == MouseEventType::MouseMove) {
  314. this->callback_mouseMoveEvent(event);
  315. } else if (event.mouseEventType == MouseEventType::Scroll) {
  316. this->callback_mouseScrollEvent(event);
  317. }
  318. }
  319. void VisualComponent::sendKeyboardEvent(const KeyboardEvent& event) {
  320. // Send the signal to a focused component or itself
  321. if (this->focusComponent.get() != nullptr) {
  322. this->focusComponent->sendKeyboardEvent(event);
  323. } else {
  324. this->receiveKeyboardEvent(event);
  325. }
  326. }
  327. void VisualComponent::receiveKeyboardEvent(const KeyboardEvent& event) {
  328. if (event.keyboardEventType == KeyboardEventType::KeyDown) {
  329. this->callback_keyDownEvent(event);
  330. } else if (event.keyboardEventType == KeyboardEventType::KeyUp) {
  331. this->callback_keyUpEvent(event);
  332. } else if (event.keyboardEventType == KeyboardEventType::KeyType) {
  333. this->callback_keyTypeEvent(event);
  334. }
  335. }
  336. void VisualComponent::applyTheme(VisualTheme theme) {
  337. this->theme = theme;
  338. this->changedTheme(theme);
  339. for (int i = 0; i < this->getChildCount(); i++) {
  340. this->children[i] -> applyTheme(theme);
  341. }
  342. }
  343. VisualTheme VisualComponent::getTheme() const {
  344. return this->theme;
  345. }
  346. void VisualComponent::changedTheme(VisualTheme newTheme) {}
  347. String VisualComponent::call(const ReadableString &methodName, const ReadableString &arguments) {
  348. throwError("Unimplemented custom call received");
  349. return U"";
  350. }
  351. bool VisualComponent::isFocused() {
  352. if (this->parent != nullptr) {
  353. // For child component, go back to the root and then follow the focus pointers to find out which component is focused within the whole tree.
  354. // One cannot just check if the parent points back directly, because old pointers may be left from a previous route.
  355. VisualComponent *root = this; while (root->parent != nullptr) { root = root->parent; }
  356. VisualComponent *leaf = root; while (leaf->focusComponent.get() != nullptr) { leaf = leaf->focusComponent.get(); }
  357. return leaf == this; // Focused if the root component points back to this component and not any further.
  358. } else {
  359. // Root component is focused if it does not redirect its focus to a child component.
  360. return this->focusComponent.get() == nullptr; // Focused if no child is focused.
  361. }
  362. }
  363. bool VisualComponent::containsFocused() {
  364. if (this->parent != nullptr) {
  365. // For child component, go back to the root and then follow the focus pointers to find out which component is focused within the whole tree.
  366. // One cannot just check if the parent points back directly, because old pointers may be left from a previous route.
  367. VisualComponent *root = this; while (root->parent != nullptr) { root = root->parent; }
  368. VisualComponent *current = root;
  369. while (current->focusComponent.get() != nullptr) {
  370. current = current->focusComponent.get();
  371. if (current == this) return true; // Focused if the root component points back to this component somewhere along the way.
  372. }
  373. return false;
  374. } else {
  375. // Root component always contains the focused component is focused if it does not redirect its focus to a child component.
  376. return this->focusComponent.get() == nullptr; // Focused if no child is focused.
  377. }
  378. }
  379. MediaResult dsr::component_generateImage(VisualTheme theme, MediaMethod &method, int width, int height, int red, int green, int blue, int pressed, int focused, int hover) {
  380. return method.callUsingKeywords([&theme, &method, width, height, red, green, blue, pressed, focused, hover](MediaMachine &machine, int methodIndex, int inputIndex, const ReadableString &argumentName){
  381. if (string_caseInsensitiveMatch(argumentName, U"width")) {
  382. machine_setInputByIndex(machine, methodIndex, inputIndex, width);
  383. } else if (string_caseInsensitiveMatch(argumentName, U"height")) {
  384. machine_setInputByIndex(machine, methodIndex, inputIndex, height);
  385. } else if (string_caseInsensitiveMatch(argumentName, U"pressed")) {
  386. machine_setInputByIndex(machine, methodIndex, inputIndex, pressed);
  387. } else if (string_caseInsensitiveMatch(argumentName, U"focused")) {
  388. machine_setInputByIndex(machine, methodIndex, inputIndex, focused);
  389. } else if (string_caseInsensitiveMatch(argumentName, U"hover")) {
  390. machine_setInputByIndex(machine, methodIndex, inputIndex, hover);
  391. } else if (string_caseInsensitiveMatch(argumentName, U"red")) {
  392. machine_setInputByIndex(machine, methodIndex, inputIndex, red);
  393. } else if (string_caseInsensitiveMatch(argumentName, U"green")) {
  394. machine_setInputByIndex(machine, methodIndex, inputIndex, green);
  395. } else if (string_caseInsensitiveMatch(argumentName, U"blue")) {
  396. machine_setInputByIndex(machine, methodIndex, inputIndex, blue);
  397. } else if (theme_assignMediaMachineArguments(theme, method.contextIndex, machine, methodIndex, inputIndex, argumentName)) {
  398. // Assigned by theme_assignMediaMachineArguments.
  399. } else {
  400. // TODO: Ask the theme for the argument using a specified style class for variations between different types of buttons, checkboxes, panels, et cetera.
  401. // Throw an exception if the theme did not provide an input argument to its own media function.
  402. throwError(U"Unhandled setting \"", argumentName, U"\" requested by the media method \"", machine_getMethodName(machine, methodIndex), U"\" in the visual theme!\n");
  403. }
  404. });
  405. }