TextBox.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  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. target.declareAttribute(U"MultiLine");
  34. }
  35. Persistent* TextBox::findAttribute(const ReadableString &name) {
  36. if (string_caseInsensitiveMatch(name, U"BackColor")) {
  37. return &(this->backColor);
  38. } else if (string_caseInsensitiveMatch(name, U"ForeColor")) {
  39. return &(this->foreColor);
  40. } else if (string_caseInsensitiveMatch(name, U"Text")) {
  41. return &(this->text);
  42. } else if (string_caseInsensitiveMatch(name, U"MultiLine")) {
  43. return &(this->multiLine);
  44. } else {
  45. return VisualComponent::findAttribute(name);
  46. }
  47. }
  48. TextBox::TextBox() {}
  49. bool TextBox::isContainer() const {
  50. return false;
  51. }
  52. // Limit exclusive indices to the text.
  53. void TextBox::limitSelection() {
  54. int64_t textLength = string_length(this->text.value);
  55. if (this->selectionStart < 0) this->selectionStart = 0;
  56. if (this->beamLocation < 0) this->beamLocation = 0;
  57. if (this->selectionStart > textLength) this->selectionStart = textLength;
  58. if (this->beamLocation > textLength) this->beamLocation = textLength;
  59. }
  60. static void tabJump(int64_t &x, int64_t leftOrigin, int64_t tabWidth) {
  61. x += tabWidth - ((x - leftOrigin) % tabWidth);
  62. }
  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. // Iterate over the whole text once for both selection and characters.
  80. // Returns the beam's X location in pixels relative to the parent of originX.
  81. int64_t printMonospaceLine(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) {
  82. int64_t characterHeight = bottomY - topY;
  83. int64_t beamPixelX = originX;
  84. 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) {
  85. if (index == beamIndex) beamPixelX = left;
  86. if (focused && selectionLeft <= index && index < selectionRight) {
  87. draw_rectangle(target, IRect(left, topY, right - left, characterHeight), ColorRgbaI32(0, 0, 100, 255));
  88. font_printCharacter(target, font, code, IVector2D(left, topY), ColorRgbaI32(255, 255, 255, 255));
  89. } else {
  90. font_printCharacter(target, font, code, IVector2D(left, topY), foreColor);
  91. }
  92. });
  93. return beamPixelX;
  94. }
  95. void TextBox::updateLines() {
  96. // Index the lines for fast scrolling and rendering.
  97. this->lines.clear();
  98. int64_t sectionStart = 0;
  99. int64_t textLength = string_length(this->text.value);
  100. for (int64_t i = 0; i < textLength; i++) {
  101. if (this->text.value[i] == U'\n') {
  102. this->lines.pushConstruct(sectionStart, i);
  103. sectionStart = i + 1;
  104. }
  105. }
  106. // Always include the line after a linebreak, even if it is empty.
  107. this->lines.pushConstruct(sectionStart, textLength);
  108. }
  109. LVector2D TextBox::getTextOrigin() {
  110. int64_t rowStride = font_getSize(this->font);
  111. int64_t halfRowStride = rowStride / 2;
  112. int64_t firstVisibleLine = this->verticalScroll / rowStride;
  113. return LVector2D(halfRowStride - this->horizontalScroll, this->multiLine.value ? (halfRowStride + (firstVisibleLine * rowStride) - this->verticalScroll) : ((image_getHeight(this->image) / 2) - halfRowStride));
  114. }
  115. // TODO: Reuse scaled background images as a separate layer.
  116. // TODO: Allow using different colors for beam, selection, selected text, normal text...
  117. // Maybe ask a separate color palette for specific things using the specific class of textboxes.
  118. // Color palettes can be independent of the media machine, allowing them to be mixed freely with different themes.
  119. // Color palettes can be loaded together with the layout to instantly have the requested standard colors by name.
  120. // Color palettes can have a standard column order of input to easily pack multiple color themes into the same color palette image.
  121. // Just a long list of names for the different X coordinates and the user selects a Y coordinate as the color theme.
  122. // New components will have to use existing parts of the palette by keeping the names reusable.
  123. // Separate components should be able to override any color for programmability, but default values should refer to the current color palette.
  124. // If no color is assigned, the class will give it a standard color from the theme.
  125. // Should classes be separate for themes and palettes?
  126. void TextBox::generateGraphics() {
  127. int width = this->location.width();
  128. int height = this->location.height();
  129. if (width < 1) { width = 1; }
  130. if (height < 1) { height = 1; }
  131. bool focused = this->isFocused();
  132. if (!this->indexedLines) {
  133. this->updateLines();
  134. this->indexedLines = true;
  135. }
  136. if (!this->hasImages || this->drawnAsFocused != focused) {
  137. this->hasImages = true;
  138. this->drawnAsFocused = focused;
  139. completeAssets();
  140. ColorRgbaI32 backColor = ColorRgbaI32(this->backColor.value, 255);
  141. ColorRgbaI32 foreColor = ColorRgbaI32(this->foreColor.value, 255);
  142. // Create a scaled image
  143. this->generateImage(this->textBox, width, height, backColor.red, backColor.green, backColor.blue, 0, focused ? 1 : 0)(this->image);
  144. this->limitSelection();
  145. LVector2D origin = this->getTextOrigin();
  146. int64_t rowStride = font_getSize(this->font);
  147. int64_t targetHeight = image_getHeight(this->image);
  148. int64_t firstVisibleLine = this->verticalScroll / rowStride;
  149. // Find character indices for left and right sides.
  150. int64_t selectionLeft = std::min(this->selectionStart, this->beamLocation);
  151. int64_t selectionRight = std::max(this->selectionStart, this->beamLocation);
  152. bool hasSelection = selectionLeft < selectionRight;
  153. // Draw the text with selection and get the beam's pixel location.
  154. int64_t topY = origin.y;
  155. for (int row = firstVisibleLine; row < this->lines.length() && topY < targetHeight; row++) {
  156. int64_t startIndex = this->lines[row].lineStartIndex;
  157. int64_t endIndex = this->lines[row].lineEndIndex;
  158. ReadableString currentLine = string_exclusiveRange(this->text.value, startIndex, endIndex);
  159. int64_t beamPixelX = printMonospaceLine(this->image, currentLine, this->font, foreColor, focused, origin.x, selectionLeft - startIndex, selectionRight - startIndex, this->beamLocation - startIndex, topY, topY + rowStride);
  160. // Draw a beam if the textbox is focused.
  161. if (focused && this->beamLocation >= startIndex && this->beamLocation <= endIndex) {
  162. int64_t beamWidth = 2;
  163. draw_rectangle(this->image, IRect(beamPixelX - 1, topY - 1, beamWidth, rowStride + 2), hasSelection ? ColorRgbaI32(255, 255, 255, 255) : foreColor);
  164. }
  165. topY += rowStride;
  166. }
  167. }
  168. }
  169. void TextBox::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
  170. this->generateGraphics();
  171. draw_copy(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
  172. }
  173. int64_t TextBox::findBeamLocationInLine(int64_t rowIndex, int64_t pixelX) {
  174. LVector2D origin = this->getTextOrigin();
  175. // Clamp to the closest row if going outside.
  176. if (rowIndex < 0) rowIndex = 0;
  177. if (rowIndex >= this->lines.length()) rowIndex = this->lines.length() - 1;
  178. int64_t beamIndex = 0;
  179. int64_t closestDistance = 1000000000000;
  180. int64_t startIndex = this->lines[rowIndex].lineStartIndex;
  181. int64_t endIndex = this->lines[rowIndex].lineEndIndex;
  182. ReadableString currentLine = string_exclusiveRange(this->text.value, startIndex, endIndex);
  183. iterateCharacters(currentLine, font, origin.x, [&beamIndex, &closestDistance, pixelX](int64_t index, DsrChar code, int64_t left, int64_t right) {
  184. int64_t center = (left + right) / 2;
  185. int64_t newDistance = std::abs(pixelX - center);
  186. if (newDistance < closestDistance) {
  187. beamIndex = index;
  188. closestDistance = newDistance;
  189. }
  190. });
  191. return startIndex + beamIndex;
  192. }
  193. int64_t TextBox::findBeamLocation(const LVector2D &pixelLocation) {
  194. LVector2D origin = this->getTextOrigin();
  195. int64_t rowStride = font_getSize(this->font);
  196. int64_t rowIndex = (pixelLocation.y - origin.y) / rowStride;
  197. return this->findBeamLocationInLine(rowIndex, pixelLocation.x);
  198. }
  199. void TextBox::receiveMouseEvent(const MouseEvent& event) {
  200. if (event.mouseEventType == MouseEventType::MouseDown) {
  201. this->mousePressed = true;
  202. int64_t newBeamIndex = findBeamLocation(LVector2D(event.position.x - this->location.left(), event.position.y - this->location.top()));
  203. if (newBeamIndex != this->selectionStart || newBeamIndex != this->beamLocation) {
  204. this->selectionStart = newBeamIndex;
  205. this->beamLocation = newBeamIndex;
  206. this->hasImages = false;
  207. }
  208. } else if (this->mousePressed && event.mouseEventType == MouseEventType::MouseMove) {
  209. if (this->mousePressed) {
  210. int64_t newBeamIndex = findBeamLocation(LVector2D(event.position.x - this->location.left(), event.position.y - this->location.top()));
  211. if (newBeamIndex != this->beamLocation) {
  212. this->beamLocation = newBeamIndex;
  213. this->hasImages = false;
  214. }
  215. }
  216. } else if (this->mousePressed && event.mouseEventType == MouseEventType::MouseUp) {
  217. this->mousePressed = false;
  218. }
  219. VisualComponent::receiveMouseEvent(event);
  220. }
  221. void TextBox::replaceSelection(const ReadableString replacingText) {
  222. int64_t selectionLeft = std::min(this->selectionStart, this->beamLocation);
  223. int64_t selectionRight = std::max(this->selectionStart, this->beamLocation);
  224. this->text.value = string_combine(string_before(this->text.value, selectionLeft), replacingText, string_from(this->text.value, selectionRight));
  225. // Place beam on the right side of the replacement without selecting anything
  226. this->selectionStart = selectionLeft + string_length(replacingText);
  227. this->beamLocation = selectionStart;
  228. this->indexedLines = false;
  229. this->hasImages = false;
  230. }
  231. void TextBox::replaceSelection(DsrChar replacingCharacter) {
  232. int64_t selectionLeft = std::min(this->selectionStart, this->beamLocation);
  233. int64_t selectionRight = std::max(this->selectionStart, this->beamLocation);
  234. String newText = string_before(this->text.value, selectionLeft);
  235. string_appendChar(newText, replacingCharacter);
  236. string_append(newText, string_from(this->text.value, selectionRight));
  237. this->text.value = newText;
  238. // Place beam on the right side of the replacement without selecting anything
  239. this->selectionStart = selectionLeft + 1;
  240. this->beamLocation = selectionStart;
  241. this->indexedLines = false;
  242. this->hasImages = false;
  243. }
  244. void TextBox::placeBeamAtCharacter(int64_t characterIndex, bool removeSelection) {
  245. this->beamLocation = characterIndex;
  246. if (removeSelection) {
  247. this->selectionStart = characterIndex;
  248. }
  249. this->hasImages = false;
  250. }
  251. void TextBox::moveBeamVertically(int64_t rowIndexOffset, bool removeSelection) {
  252. // Find the current beam's row index.
  253. int64_t oldRowIndex = 0;
  254. for (int row = 0; row < this->lines.length(); row++) {
  255. int64_t startIndex = this->lines[row].lineStartIndex;
  256. int64_t endIndex = this->lines[row].lineEndIndex;
  257. if (this->beamLocation >= startIndex && this->beamLocation <= endIndex) {
  258. oldRowIndex = row;
  259. }
  260. }
  261. // Find another row.
  262. int64_t newRowIndex = oldRowIndex + rowIndexOffset;
  263. if (newRowIndex < 0) { newRowIndex = 0; }
  264. if (newRowIndex >= this->lines.length()) { newRowIndex = this->lines.length() - 1; }
  265. // Get the old pixel offset from the beam.
  266. LVector2D origin = this->getTextOrigin();
  267. int64_t beamPixelX = 0;
  268. int64_t lineStartIndex = this->lines[oldRowIndex].lineStartIndex;
  269. int64_t lineEndIndex = this->lines[oldRowIndex].lineEndIndex;
  270. int64_t localBeamIndex = this->beamLocation - lineStartIndex;
  271. ReadableString currentLine = string_exclusiveRange(this->text.value, lineStartIndex, lineEndIndex);
  272. iterateCharacters(currentLine, font, origin.x, [&beamPixelX, localBeamIndex](int64_t index, DsrChar code, int64_t left, int64_t right) {
  273. if (index == localBeamIndex) beamPixelX = left;
  274. });
  275. printText(U"beamPixelX = ", beamPixelX, U"\n");
  276. // Get the closest location in the new row.
  277. int64_t newCharacterIndex = findBeamLocationInLine(newRowIndex, beamPixelX);
  278. printText(U"newCharacterIndex = ", newCharacterIndex, U"\n");
  279. placeBeamAtCharacter(newCharacterIndex, removeSelection);
  280. }
  281. static const uint32_t combinationKey_leftShift = 1 << 0;
  282. static const uint32_t combinationKey_rightShift = 1 << 1;
  283. static const uint32_t combinationKey_shift = combinationKey_leftShift | combinationKey_rightShift;
  284. static const uint32_t combinationKey_leftControl = 1 << 2;
  285. static const uint32_t combinationKey_rightControl = 1 << 3;
  286. static const uint32_t combinationKey_control = combinationKey_leftControl | combinationKey_rightControl;
  287. static int64_t getLineStart(const ReadableString &text, int64_t searchStart) {
  288. for (int64_t i = searchStart - 1; i >= 0; i--) {
  289. if (text[i] == U'\n') {
  290. return i + 1;
  291. }
  292. }
  293. return 0;
  294. }
  295. static int64_t getLineEnd(const ReadableString &text, int64_t searchStart) {
  296. for (int64_t i = searchStart; i < string_length(text); i++) {
  297. if (text[i] == U'\n') {
  298. return i;
  299. }
  300. }
  301. return string_length(text);
  302. }
  303. // TODO: Copy and paste using a clipboard. (With automatic removal of new lines when multi-line is disabled)
  304. void TextBox::receiveKeyboardEvent(const KeyboardEvent& event) {
  305. // Insert and scroll-lock is not supported.
  306. if (event.keyboardEventType == KeyboardEventType::KeyDown) {
  307. if (event.dsrKey == DsrKey_LeftShift) {
  308. this->combinationKeys |= combinationKey_leftShift;
  309. } else if (event.dsrKey == DsrKey_RightShift) {
  310. this->combinationKeys |= combinationKey_rightShift;
  311. } else if (event.dsrKey == DsrKey_LeftControl) {
  312. this->combinationKeys |= combinationKey_leftControl;
  313. } else if (event.dsrKey == DsrKey_RightControl) {
  314. this->combinationKeys |= combinationKey_rightControl;
  315. }
  316. } else if (event.keyboardEventType == KeyboardEventType::KeyUp) {
  317. if (event.dsrKey == DsrKey_LeftShift) {
  318. this->combinationKeys &= ~combinationKey_leftShift;
  319. } else if (event.dsrKey == DsrKey_RightShift) {
  320. this->combinationKeys &= ~combinationKey_rightShift;
  321. } else if (event.dsrKey == DsrKey_LeftControl) {
  322. this->combinationKeys &= ~combinationKey_leftControl;
  323. } else if (event.dsrKey == DsrKey_RightControl) {
  324. this->combinationKeys &= ~combinationKey_rightControl;
  325. }
  326. } else if (event.keyboardEventType == KeyboardEventType::KeyType) {
  327. int64_t textLength = string_length(this->text.value);
  328. bool selected = this->selectionStart != this->beamLocation;
  329. bool printable = event.character == U'\t' || (31 < event.character && event.character < 127) || 159 < event.character;
  330. bool canGoLeft = textLength > 0 && this->beamLocation > 0;
  331. bool canGoRight = textLength > 0 && this->beamLocation < textLength;
  332. bool holdShift = this->combinationKeys & combinationKey_shift;
  333. bool holdControl = this->combinationKeys & combinationKey_control;
  334. bool removeSelection = !holdShift;
  335. if (selected && (event.dsrKey == DsrKey_BackSpace || event.dsrKey == DsrKey_Delete)) {
  336. // Remove selection
  337. this->replaceSelection(U"");
  338. } else if (event.dsrKey == DsrKey_BackSpace && canGoLeft) {
  339. // Erase left of beam
  340. this->beamLocation--;
  341. this->replaceSelection(U"");
  342. } else if (event.dsrKey == DsrKey_Delete && canGoRight) {
  343. // Erase right of beam
  344. this->beamLocation++;
  345. this->replaceSelection(U"");
  346. } else if (event.dsrKey == DsrKey_Home || (event.dsrKey == DsrKey_LeftArrow && holdControl)) {
  347. // Move to the line start using Home or Ctrl + LeftArrow
  348. this->placeBeamAtCharacter(getLineStart(this->text.value, this->beamLocation), removeSelection);
  349. } else if (event.dsrKey == DsrKey_End || (event.dsrKey == DsrKey_RightArrow && holdControl)) {
  350. // Move to the line end using End or Ctrl + RightArrow
  351. this->placeBeamAtCharacter(getLineEnd(this->text.value, this->beamLocation), removeSelection);
  352. } else if (event.dsrKey == DsrKey_LeftArrow && canGoLeft) {
  353. // Move left using LeftArrow
  354. this->placeBeamAtCharacter(this->beamLocation - 1, removeSelection);
  355. } else if (event.dsrKey == DsrKey_RightArrow && canGoRight) {
  356. // Move right using RightArrow
  357. this->placeBeamAtCharacter(this->beamLocation + 1, removeSelection);
  358. } else if (event.dsrKey == DsrKey_UpArrow) {
  359. // Move up using UpArrow
  360. this->moveBeamVertically(-1, removeSelection);
  361. } else if (event.dsrKey == DsrKey_DownArrow) {
  362. // Move down using DownArrow
  363. this->moveBeamVertically(1, removeSelection);
  364. } else if (event.dsrKey == DsrKey_Return) {
  365. if (this->multiLine.value) {
  366. this->replaceSelection(U'\n');
  367. }
  368. } else if (printable) {
  369. this->replaceSelection(event.character);
  370. }
  371. //printText(U"KeyType char=", event.character, " key=", event.dsrKey, U"\n");
  372. }
  373. VisualComponent::receiveKeyboardEvent(event);
  374. }
  375. bool TextBox::pointIsInside(const IVector2D& pixelPosition) {
  376. this->generateGraphics();
  377. // Get the point relative to the component instead of its direct container
  378. IVector2D localPoint = pixelPosition - this->location.upperLeft();
  379. // Sample opacity at the location
  380. return dsr::image_readPixel_border(this->image, localPoint.x, localPoint.y).alpha > 127;
  381. }
  382. void TextBox::changedTheme(VisualTheme newTheme) {
  383. this->textBox = theme_getScalableImage(newTheme, U"TextBox");
  384. this->hasImages = false;
  385. }
  386. void TextBox::completeAssets() {
  387. if (this->textBox.methodIndex == -1) {
  388. this->textBox = theme_getScalableImage(theme_getDefault(), U"TextBox");
  389. }
  390. if (this->font.get() == nullptr) {
  391. this->font = font_getDefault();
  392. }
  393. }
  394. void TextBox::changedLocation(const IRect &oldLocation, const IRect &newLocation) {
  395. // If the component has changed dimensions then redraw the image
  396. if (oldLocation.size() != newLocation.size()) {
  397. this->hasImages = false;
  398. }
  399. }
  400. void TextBox::changedAttribute(const ReadableString &name) {
  401. if (!string_caseInsensitiveMatch(name, U"Visible")) {
  402. this->hasImages = false;
  403. if (string_caseInsensitiveMatch(name, U"Text")) {
  404. this->limitSelection();
  405. this->indexedLines = false;
  406. }
  407. }
  408. }