TextBox.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. // zlib open source license
  2. //
  3. // Copyright (c) 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 "TextBox.h"
  24. #include <math.h>
  25. #include <functional>
  26. using namespace dsr;
  27. PERSISTENT_DEFINITION(TextBox)
  28. void TextBox::declareAttributes(StructureDefinition &target) const {
  29. VisualComponent::declareAttributes(target);
  30. target.declareAttribute(U"BackColor");
  31. target.declareAttribute(U"ForeColor");
  32. target.declareAttribute(U"Text");
  33. }
  34. Persistent* TextBox::findAttribute(const ReadableString &name) {
  35. if (string_caseInsensitiveMatch(name, U"BackColor")) {
  36. return &(this->backColor);
  37. } else if (string_caseInsensitiveMatch(name, U"ForeColor")) {
  38. return &(this->foreColor);
  39. } else if (string_caseInsensitiveMatch(name, U"Text")) {
  40. return &(this->text);
  41. } else if (string_caseInsensitiveMatch(name, U"MultiLine")) {
  42. return &(this->multiLine);
  43. } else {
  44. return VisualComponent::findAttribute(name);
  45. }
  46. }
  47. TextBox::TextBox() {}
  48. bool TextBox::isContainer() const {
  49. return false;
  50. }
  51. // Limit exclusive indices to the text.
  52. void TextBox::limitSelection() {
  53. int64_t textLength = string_length(this->text.value);
  54. if (this->selectionStart < 0) this->selectionStart = 0;
  55. if (this->beamLocation < 0) this->beamLocation = 0;
  56. if (this->selectionStart > textLength) this->selectionStart = textLength;
  57. if (this->beamLocation > textLength) this->beamLocation = textLength;
  58. }
  59. static void tabJump(int64_t &x, int64_t leftOrigin, int64_t tabWidth) {
  60. x += tabWidth - ((x - leftOrigin) % tabWidth);
  61. }
  62. // TODO: Make a separate version for multi-line textboxes.
  63. // To have a stable tab alignment, the whole text must be given when iterating.
  64. void iterateCharacters(const ReadableString& text, const RasterFont &font, int64_t originX, std::function<void(int64_t index, DsrChar code, int64_t left, int64_t right)> characterAction) {
  65. int64_t right = originX;
  66. int64_t tabWidth = font_getTabWidth(font);
  67. int64_t monospaceWidth = font_getMonospaceWidth(font);
  68. for (int64_t i = 0; i <= string_length(text); i++) {
  69. DsrChar code = text[i];
  70. int64_t left = right;
  71. if (code == U'\t') {
  72. tabJump(right, originX, tabWidth);
  73. } else {
  74. right += monospaceWidth;
  75. }
  76. characterAction(i, code, left, right);
  77. }
  78. }
  79. int64_t findBeamLocation(const ReadableString& text, const RasterFont &font, int64_t originX, int64_t findPixelX) {
  80. int64_t beamIndex = 0;
  81. int64_t closestDistance = 1000000000000;
  82. iterateCharacters(text, font, originX, [&beamIndex, &closestDistance, findPixelX](int64_t index, DsrChar code, int64_t left, int64_t right) {
  83. // TODO: Why is selection not centered? Is it the origin handled differently?
  84. int64_t center = (left + right) / 2;
  85. int64_t newDistance = std::abs(findPixelX - center);
  86. if (newDistance < closestDistance) {
  87. beamIndex = index;
  88. closestDistance = newDistance;
  89. }
  90. });
  91. return beamIndex;
  92. }
  93. // Iterate over the whole text once for both selection and characters.
  94. // Returns the beam's X location in pixels relative to the parent of originX.
  95. int64_t printMonospace(OrderedImageRgbaU8 &target, const ReadableString& text, const RasterFont &font, ColorRgbaI32 foreColor, bool focused, int64_t originX, int64_t selectionLeft, int64_t selectionRight, int64_t beamIndex, int64_t topY, int64_t bottomY) {
  96. int64_t characterHeight = bottomY - topY;
  97. int64_t beamPixelX = originX;
  98. iterateCharacters(text, font, originX, [&target, &font, &foreColor, &beamPixelX, selectionLeft, selectionRight, beamIndex, topY, characterHeight, focused](int64_t index, DsrChar code, int64_t left, int64_t right) {
  99. if (index == beamIndex) beamPixelX = left;
  100. if (focused && selectionLeft <= index && index < selectionRight) {
  101. draw_rectangle(target, IRect(left, topY, right - left, characterHeight), ColorRgbaI32(0, 0, 100, 255));
  102. font_printCharacter(target, font, code, IVector2D(left, topY), ColorRgbaI32(255, 255, 255, 255));
  103. } else {
  104. font_printCharacter(target, font, code, IVector2D(left, topY), foreColor);
  105. }
  106. });
  107. return beamPixelX;
  108. }
  109. // TODO: Reuse scaled background images as a separate layer.
  110. // TODO: Allow using different colors for beam, selection, selected text, normal text...
  111. // Maybe ask a separate color palette for specific things using the specific class of textboxes.
  112. // Color palettes can be independent of the media machine, allowing them to be mixed freely with different themes.
  113. // Color palettes can be loaded together with the layout to instantly have the requested standard colors by name.
  114. // Color palettes can have a standard column order of input to easily pack multiple color themes into the same color palette image.
  115. // Just a long list of names for the different X coordinates and the user selects a Y coordinate as the color theme.
  116. // New components will have to use existing parts of the palette by keeping the names reusable.
  117. // Separate components should be able to override any color for programmability, but default values should refer to the current color palette.
  118. // If no color is assigned, the class will give it a standard color from the theme.
  119. // Should classes be separate for themes and palettes?
  120. static OrderedImageRgbaU8 generateBoxImage(TextBox &textBox, MediaMethod imageGenerator, bool focused, int width, int height, ColorRgbaI32 backColor, ColorRgbaI32 foreColor, const ReadableString &text, const RasterFont &font) {
  121. // Create a scaled image
  122. OrderedImageRgbaU8 result;
  123. textBox.generateImage(imageGenerator, width, height, backColor.red, backColor.green, backColor.blue, 0, focused ? 1 : 0)(result);
  124. textBox.limitSelection();
  125. // TODO: Allow moving the viewport to follow longer input.
  126. // TODO: Allow multi-line textboxes with scrollbars.
  127. // The logic of scrollbars must be reused as value allocated objects across components, but with different settings.
  128. int64_t halfFontSize = font_getSize(font) / 2;
  129. int64_t originX = halfFontSize;
  130. int64_t center = image_getHeight(result) / 2;
  131. int64_t topY = center - halfFontSize;
  132. int64_t bottomY = center + halfFontSize;
  133. // Find character indices for left and right sides.
  134. int64_t selectionLeft = std::min(textBox.selectionStart, textBox.beamLocation);
  135. int64_t selectionRight = std::max(textBox.selectionStart, textBox.beamLocation);
  136. bool hasSelection = selectionLeft < selectionRight;
  137. // Draw the text with selection and get the beam's pixel location.
  138. int64_t beamPixelX = printMonospace(result, text, font, foreColor, focused, originX, selectionLeft, selectionRight, textBox.beamLocation, topY, bottomY);
  139. // Draw a beam if the textbox is focused.
  140. if (focused) {
  141. int64_t beamWidth = 2;
  142. draw_rectangle(result, IRect(beamPixelX - 1, topY - 1, beamWidth, bottomY - topY + 2), hasSelection ? ColorRgbaI32(255, 255, 255, 255) : foreColor);
  143. }
  144. return result;
  145. }
  146. void TextBox::generateGraphics() {
  147. int width = this->location.width();
  148. int height = this->location.height();
  149. if (width < 1) { width = 1; }
  150. if (height < 1) { height = 1; }
  151. bool currentlyFocused = this->isFocused();
  152. if (!this->hasImages || this->drawnAsFocused != currentlyFocused) {
  153. completeAssets();
  154. this->image = generateBoxImage(*this, this->textBox, currentlyFocused, width, height, ColorRgbaI32(this->backColor.value, 255), ColorRgbaI32(this->foreColor.value, 255), this->text.value, this->font);
  155. this->hasImages = true;
  156. this->drawnAsFocused = currentlyFocused;
  157. }
  158. }
  159. void TextBox::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
  160. this->generateGraphics();
  161. draw_copy(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
  162. }
  163. void TextBox::receiveMouseEvent(const MouseEvent& event) {
  164. int64_t originX = font_getSize(this->font) / 2;
  165. int32_t localMouseX = event.position.x - this->location.left();
  166. if (event.mouseEventType == MouseEventType::MouseDown) {
  167. this->mousePressed = true;
  168. int64_t newBeamIndex = findBeamLocation(this->text.value, this->font, originX, localMouseX);
  169. if (newBeamIndex != this->selectionStart || newBeamIndex != this->beamLocation) {
  170. this->selectionStart = newBeamIndex;
  171. this->beamLocation = newBeamIndex;
  172. this->hasImages = false;
  173. }
  174. } else if (this->mousePressed && event.mouseEventType == MouseEventType::MouseMove) {
  175. if (this->mousePressed) {
  176. int64_t newBeamIndex = findBeamLocation(this->text.value, this->font, originX, localMouseX);
  177. if (newBeamIndex != this->beamLocation) {
  178. this->beamLocation = newBeamIndex;
  179. this->hasImages = false;
  180. }
  181. }
  182. } else if (this->mousePressed && event.mouseEventType == MouseEventType::MouseUp) {
  183. this->mousePressed = false;
  184. }
  185. VisualComponent::receiveMouseEvent(event);
  186. }
  187. void TextBox::replaceSelection(const ReadableString replacingText) {
  188. int64_t selectionLeft = std::min(this->selectionStart, this->beamLocation);
  189. int64_t selectionRight = std::max(this->selectionStart, this->beamLocation);
  190. this->text.value = string_combine(string_before(this->text.value, selectionLeft), replacingText, string_from(this->text.value, selectionRight));
  191. // Place beam on the right side of the replacement without selecting anything
  192. this->selectionStart = selectionLeft + string_length(replacingText);
  193. this->beamLocation = selectionStart;
  194. this->hasImages = false;
  195. }
  196. void TextBox::replaceSelection(DsrChar replacingCharacter) {
  197. int64_t selectionLeft = std::min(this->selectionStart, this->beamLocation);
  198. int64_t selectionRight = std::max(this->selectionStart, this->beamLocation);
  199. String newText = string_before(this->text.value, selectionLeft);
  200. string_appendChar(newText, replacingCharacter);
  201. string_append(newText, string_from(this->text.value, selectionRight));
  202. this->text.value = newText;
  203. // Place beam on the right side of the replacement without selecting anything
  204. this->selectionStart = selectionLeft + 1;
  205. this->beamLocation = selectionStart;
  206. this->hasImages = false;
  207. }
  208. void TextBox::placeBeam(int64_t index, bool removeSelection) {
  209. this->beamLocation = index;
  210. if (removeSelection) {
  211. this->selectionStart = index;
  212. }
  213. this->hasImages = false;
  214. }
  215. static const uint32_t combinationKey_leftShift = 1 << 0;
  216. static const uint32_t combinationKey_rightShift = 1 << 1;
  217. static const uint32_t combinationKey_shift = combinationKey_leftShift | combinationKey_rightShift;
  218. static const uint32_t combinationKey_leftControl = 1 << 2;
  219. static const uint32_t combinationKey_rightControl = 1 << 3;
  220. static const uint32_t combinationKey_control = combinationKey_leftControl | combinationKey_rightControl;
  221. // TODO: Copy and paste using a clipboard.
  222. void TextBox::receiveKeyboardEvent(const KeyboardEvent& event) {
  223. // Insert and scroll-lock is not supported.
  224. if (event.keyboardEventType == KeyboardEventType::KeyDown) {
  225. if (event.dsrKey == DsrKey_LeftShift) {
  226. this->combinationKeys |= combinationKey_leftShift;
  227. } else if (event.dsrKey == DsrKey_RightShift) {
  228. this->combinationKeys |= combinationKey_rightShift;
  229. } else if (event.dsrKey == DsrKey_LeftControl) {
  230. this->combinationKeys |= combinationKey_leftControl;
  231. } else if (event.dsrKey == DsrKey_RightControl) {
  232. this->combinationKeys |= combinationKey_rightControl;
  233. }
  234. } else if (event.keyboardEventType == KeyboardEventType::KeyUp) {
  235. if (event.dsrKey == DsrKey_LeftShift) {
  236. this->combinationKeys &= ~combinationKey_leftShift;
  237. } else if (event.dsrKey == DsrKey_RightShift) {
  238. this->combinationKeys &= ~combinationKey_rightShift;
  239. } else if (event.dsrKey == DsrKey_LeftControl) {
  240. this->combinationKeys &= ~combinationKey_leftControl;
  241. } else if (event.dsrKey == DsrKey_RightControl) {
  242. this->combinationKeys &= ~combinationKey_rightControl;
  243. }
  244. } else if (event.keyboardEventType == KeyboardEventType::KeyType) {
  245. int64_t textLength = string_length(this->text.value);
  246. bool selected = this->selectionStart != this->beamLocation;
  247. bool printable = event.character == U'\t' || (31 < event.character && event.character < 127) || 159 < event.character;
  248. bool canGoLeft = textLength > 0 && this->beamLocation > 0;
  249. bool canGoRight = textLength > 0 && this->beamLocation < textLength;
  250. bool holdShift = this->combinationKeys & combinationKey_shift;
  251. bool holdControl = this->combinationKeys & combinationKey_control;
  252. bool removeSelection = !holdShift;
  253. if (selected && (event.dsrKey == DsrKey_BackSpace || event.dsrKey == DsrKey_Delete)) {
  254. // Remove selection
  255. this->replaceSelection(U"");
  256. } else if (event.dsrKey == DsrKey_BackSpace && canGoLeft) {
  257. // Erase left of beam
  258. this->beamLocation--;
  259. this->replaceSelection(U"");
  260. } else if (event.dsrKey == DsrKey_Delete && canGoRight) {
  261. // Erase right of beam
  262. this->beamLocation++;
  263. this->replaceSelection(U"");
  264. } else if (event.dsrKey == DsrKey_Home || (event.dsrKey == DsrKey_LeftArrow && holdControl)) {
  265. // Move to the start using Home or Ctrl + LeftArrow
  266. this->placeBeam(0, removeSelection);
  267. } else if (event.dsrKey == DsrKey_End || (event.dsrKey == DsrKey_RightArrow && holdControl)) {
  268. // Move to the end using End or Ctrl + RightArrow
  269. this->placeBeam(textLength, removeSelection);
  270. } else if (event.dsrKey == DsrKey_LeftArrow && canGoLeft) {
  271. // Move left using LeftArrow
  272. this->placeBeam(this->beamLocation - 1, removeSelection);
  273. } else if (event.dsrKey == DsrKey_RightArrow && canGoRight) {
  274. // Move right using RightArrow
  275. this->placeBeam(this->beamLocation + 1, removeSelection);
  276. } else if (printable) {
  277. this->replaceSelection(event.character);
  278. }
  279. //printText(U"KeyType char=", event.character, " key=", event.dsrKey, U"\n");
  280. }
  281. VisualComponent::receiveKeyboardEvent(event);
  282. }
  283. bool TextBox::pointIsInside(const IVector2D& pixelPosition) {
  284. this->generateGraphics();
  285. // Get the point relative to the component instead of its direct container
  286. IVector2D localPoint = pixelPosition - this->location.upperLeft();
  287. // Sample opacity at the location
  288. return dsr::image_readPixel_border(this->image, localPoint.x, localPoint.y).alpha > 127;
  289. }
  290. void TextBox::changedTheme(VisualTheme newTheme) {
  291. this->textBox = theme_getScalableImage(newTheme, U"TextBox");
  292. this->hasImages = false;
  293. }
  294. void TextBox::completeAssets() {
  295. if (this->textBox.methodIndex == -1) {
  296. this->textBox = theme_getScalableImage(theme_getDefault(), U"TextBox");
  297. }
  298. if (this->font.get() == nullptr) {
  299. this->font = font_getDefault();
  300. }
  301. }
  302. void TextBox::changedLocation(const IRect &oldLocation, const IRect &newLocation) {
  303. // If the component has changed dimensions then redraw the image
  304. if (oldLocation.size() != newLocation.size()) {
  305. this->hasImages = false;
  306. }
  307. }
  308. void TextBox::changedAttribute(const ReadableString &name) {
  309. if (!string_caseInsensitiveMatch(name, U"Visible")) {
  310. this->hasImages = false;
  311. if (string_caseInsensitiveMatch(name, U"Text")) {
  312. this->limitSelection();
  313. }
  314. }
  315. }