TextBox.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  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 <functional>
  25. using namespace dsr;
  26. PERSISTENT_DEFINITION(TextBox)
  27. void TextBox::declareAttributes(StructureDefinition &target) const {
  28. VisualComponent::declareAttributes(target);
  29. target.declareAttribute(U"BackColor");
  30. target.declareAttribute(U"ForeColor");
  31. target.declareAttribute(U"Text");
  32. target.declareAttribute(U"MultiLine");
  33. target.declareAttribute(U"BackgroundClass");
  34. }
  35. Persistent* TextBox::findAttribute(const ReadableString &name) {
  36. if (string_caseInsensitiveMatch(name, U"Color") || 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 if (string_caseInsensitiveMatch(name, U"Class") || string_caseInsensitiveMatch(name, U"BackgroundClass")) {
  45. return &(this->backgroundClass);
  46. } else {
  47. return VisualComponent::findAttribute(name);
  48. }
  49. }
  50. TextBox::TextBox() {}
  51. bool TextBox::isContainer() const {
  52. return false;
  53. }
  54. // Limit exclusive indices to the text.
  55. void TextBox::limitSelection() {
  56. int64_t textLength = string_length(this->text.value);
  57. if (this->selectionStart < 0) this->selectionStart = 0;
  58. if (this->beamLocation < 0) this->beamLocation = 0;
  59. if (this->selectionStart > textLength) this->selectionStart = textLength;
  60. if (this->beamLocation > textLength) this->beamLocation = textLength;
  61. }
  62. static void tabJump(int64_t &x, int64_t tabWidth) {
  63. x += tabWidth - (x % tabWidth);
  64. }
  65. static int64_t monospacesPerTab = 4;
  66. // Pre-condition: text does not contain any linebreak.
  67. static void iterateCharactersInLine(const ReadableString& text, const RasterFont &font, std::function<void(int64_t index, DsrChar code, int64_t left, int64_t right)> characterAction) {
  68. int64_t right = 0;
  69. int64_t monospaceWidth = font_getMonospaceWidth(font);
  70. int64_t tabWidth = monospaceWidth * monospacesPerTab;
  71. for (int64_t i = 0; i <= string_length(text); i++) {
  72. DsrChar code = text[i];
  73. int64_t left = right;
  74. if (code == U'\t') {
  75. tabJump(right, tabWidth);
  76. } else {
  77. right += monospaceWidth;
  78. }
  79. characterAction(i, code, left, right);
  80. }
  81. }
  82. // Iterate over the whole text once for both selection and characters.
  83. // Returns the beam's X location in pixels.
  84. static 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) {
  85. int64_t characterHeight = bottomY - topY;
  86. int64_t beamPixelX = 0;
  87. iterateCharactersInLine(text, font, [&target, &font, &foreColor, &beamPixelX, originX, selectionLeft, selectionRight, beamIndex, topY, characterHeight, focused](int64_t index, DsrChar code, int64_t left, int64_t right) {
  88. left += originX;
  89. right += originX;
  90. if (index == beamIndex) beamPixelX = left;
  91. if (focused && selectionLeft <= index && index < selectionRight) {
  92. draw_rectangle(target, IRect(left, topY, right - left, characterHeight), ColorRgbaI32(0, 0, 100, 255));
  93. font_printCharacter(target, font, code, IVector2D(left, topY), ColorRgbaI32(255, 255, 255, 255));
  94. } else {
  95. font_printCharacter(target, font, code, IVector2D(left, topY), foreColor);
  96. }
  97. });
  98. return beamPixelX;
  99. }
  100. void TextBox::indexLines() {
  101. int64_t newLength = string_length(this->text.value);
  102. if (newLength != this->indexedAtLength) {
  103. int64_t currentLength = 0;
  104. int64_t worstCaseLength = 0;
  105. // Index the lines for fast scrolling and rendering.
  106. this->lines.clear();
  107. int64_t sectionStart = 0;
  108. for (int64_t i = 0; i <= newLength; i++) {
  109. DsrChar c = this->text.value[i];
  110. if (c == U'\n' || c == U'\0') {
  111. if (currentLength > worstCaseLength) {
  112. worstCaseLength = currentLength;
  113. }
  114. currentLength = 0;
  115. this->lines.pushConstruct(sectionStart, i);
  116. sectionStart = i + 1;
  117. } else if (c == U'\t') {
  118. currentLength += 4;
  119. } else {
  120. currentLength += 1;
  121. }
  122. }
  123. this->indexedAtLength = newLength;
  124. this->worstCaseLineMonospaces = worstCaseLength;
  125. }
  126. }
  127. LVector2D TextBox::getTextOrigin(bool includeVerticalScroll) {
  128. int64_t rowStride = font_getSize(this->font);
  129. int64_t offsetX = this->borderX - this->horizontalScrollBar.getValue();
  130. int64_t offsetY = 0;
  131. if (this->multiLine.value) {
  132. offsetY = this->borderY;
  133. } else {
  134. offsetY = (image_getHeight(this->image) - rowStride) / 2;
  135. }
  136. if (includeVerticalScroll) {
  137. offsetY -= this->verticalScrollBar.getValue() * rowStride;
  138. }
  139. return LVector2D(offsetX, offsetY);
  140. }
  141. // TODO: Reuse scaled background images as a separate layer.
  142. // TODO: Allow using different colors for beam, selection, selected text, normal text...
  143. // Maybe ask a separate color palette for specific things using the specific class of textboxes.
  144. // Color palettes can be independent of the media machine, allowing them to be mixed freely with different themes.
  145. // Color palettes can be loaded together with the layout to instantly have the requested standard colors by name.
  146. // Color palettes can have a standard column order of input to easily pack multiple color themes into the same color palette image.
  147. // Just a long list of names for the different X coordinates and the user selects a Y coordinate as the color theme.
  148. // New components will have to use existing parts of the palette by keeping the names reusable.
  149. // Separate components should be able to override any color for programmability, but default values should refer to the current color palette.
  150. // If no color is assigned, the class will give it a standard color from the theme.
  151. // Should classes be separate for themes and palettes?
  152. void TextBox::generateGraphics() {
  153. int32_t width = this->location.width();
  154. int32_t height = this->location.height();
  155. if (width < 1) { width = 1; }
  156. if (height < 1) { height = 1; }
  157. bool focused = this->isFocused();
  158. if (!this->hasImages || this->drawnAsFocused != focused) {
  159. this->hasImages = true;
  160. this->drawnAsFocused = focused;
  161. completeAssets();
  162. this->indexLines();
  163. ColorRgbaI32 foreColorRgba = ColorRgbaI32(this->foreColor.value, 255);
  164. // Create a scaled image
  165. component_generateImage(this->theme, this->textBox, width, height, this->backColor.value.red, this->backColor.value.green, this->backColor.value.blue, 0, focused ? 1 : 0)(this->image);
  166. this->limitSelection();
  167. LVector2D origin = this->getTextOrigin(false);
  168. int64_t rowStride = font_getSize(this->font);
  169. int64_t targetHeight = image_getHeight(this->image);
  170. int64_t firstVisibleLine = this->verticalScrollBar.getValue();
  171. // Find character indices for left and right sides.
  172. int64_t selectionLeft = std::min(this->selectionStart, this->beamLocation);
  173. int64_t selectionRight = std::max(this->selectionStart, this->beamLocation);
  174. bool hasSelection = selectionLeft < selectionRight;
  175. // Draw the text with selection and get the beam's pixel location.
  176. int64_t topY = origin.y;
  177. for (int64_t row = firstVisibleLine; row < this->lines.length() && topY < targetHeight; row++) {
  178. int64_t startIndex = this->lines[row].lineStartIndex;
  179. int64_t endIndex = this->lines[row].lineEndIndex;
  180. ReadableString currentLine = string_exclusiveRange(this->text.value, startIndex, endIndex);
  181. int64_t beamPixelX = printMonospaceLine(this->image, currentLine, this->font, foreColorRgba, focused, origin.x, selectionLeft - startIndex, selectionRight - startIndex, this->beamLocation - startIndex, topY, topY + rowStride);
  182. // Draw a beam if the textbox is focused.
  183. if (focused && this->beamLocation >= startIndex && this->beamLocation <= endIndex) {
  184. int64_t beamWidth = 2;
  185. draw_rectangle(this->image, IRect(beamPixelX - 1, topY - 1, beamWidth, rowStride + 2), hasSelection ? ColorRgbaI32(255, 255, 255, 255) : foreColorRgba);
  186. }
  187. topY += rowStride;
  188. }
  189. this->verticalScrollBar.draw(this->image, this->theme, this->backColor.value);
  190. this->horizontalScrollBar.draw(this->image, this->theme, this->backColor.value);
  191. }
  192. }
  193. void TextBox::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
  194. this->generateGraphics();
  195. if (this->background_filter == 1) {
  196. draw_alphaFilter(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
  197. } else {
  198. draw_copy(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
  199. }
  200. }
  201. int64_t TextBox::findBeamLocationInLine(int64_t rowIndex, int64_t pixelX) {
  202. LVector2D origin = this->getTextOrigin(true);
  203. // Clamp to the closest row if going outside.
  204. if (rowIndex < 0) rowIndex = 0;
  205. if (rowIndex >= this->lines.length()) rowIndex = this->lines.length() - 1;
  206. int64_t beamIndex = 0;
  207. int64_t closestDistance = 1000000000000;
  208. int64_t startIndex = this->lines[rowIndex].lineStartIndex;
  209. int64_t endIndex = this->lines[rowIndex].lineEndIndex;
  210. ReadableString currentLine = string_exclusiveRange(this->text.value, startIndex, endIndex);
  211. iterateCharactersInLine(currentLine, font, [&beamIndex, &closestDistance, &origin, pixelX](int64_t index, DsrChar code, int64_t left, int64_t right) {
  212. int64_t center = origin.x + (left + right) / 2;
  213. int64_t newDistance = std::abs(pixelX - center);
  214. if (newDistance < closestDistance) {
  215. beamIndex = index;
  216. closestDistance = newDistance;
  217. }
  218. });
  219. return startIndex + beamIndex;
  220. }
  221. BeamLocation TextBox::findBeamLocation(const LVector2D &pixelLocation) {
  222. LVector2D origin = this->getTextOrigin(true);
  223. int64_t rowStride = font_getSize(this->font);
  224. int64_t rowIndex = (pixelLocation.y - origin.y) / rowStride;
  225. return BeamLocation(rowIndex, this->findBeamLocationInLine(rowIndex, pixelLocation.x));
  226. }
  227. static int64_t findBeamRow(List<LineIndex> lines, int64_t beamLocation) {
  228. int64_t result = 0;
  229. for (int64_t row = 0; row < lines.length(); row++) {
  230. int64_t startIndex = lines[row].lineStartIndex;
  231. int64_t endIndex = lines[row].lineEndIndex;
  232. if (beamLocation >= startIndex && beamLocation <= endIndex) {
  233. result = row;
  234. }
  235. }
  236. return result;
  237. }
  238. // Returns the beam's pixel offset relative to the origin.
  239. static int64_t getBeamPixelOffset(const ReadableString &text, const RasterFont &font, List<LineIndex> lines, const BeamLocation &beam) {
  240. int64_t result = 0;
  241. int64_t lineStartIndex = lines[beam.rowIndex].lineStartIndex;
  242. int64_t lineEndIndex = lines[beam.rowIndex].lineEndIndex;
  243. int64_t localBeamIndex = beam.characterIndex - lineStartIndex;
  244. ReadableString currentLine = string_exclusiveRange(text, lineStartIndex, lineEndIndex);
  245. iterateCharactersInLine(currentLine, font, [&result, localBeamIndex](int64_t index, DsrChar code, int64_t left, int64_t right) {
  246. if (index == localBeamIndex) result = left;
  247. });
  248. return result;
  249. }
  250. void TextBox::receiveMouseEvent(const MouseEvent& event) {
  251. bool verticalScrollIntercepted = this->verticalScrollBar.receiveMouseEvent(this->location, event);
  252. bool horizontalScrollIntercepted = this->horizontalScrollBar.receiveMouseEvent(this->location, event);
  253. bool scrollIntercepted = verticalScrollIntercepted || horizontalScrollIntercepted;
  254. if (event.mouseEventType == MouseEventType::MouseDown && !scrollIntercepted) {
  255. this->mousePressed = true;
  256. BeamLocation newBeam = findBeamLocation(LVector2D(event.position.x - this->location.left(), event.position.y - this->location.top()));
  257. if (newBeam.characterIndex != this->selectionStart || newBeam.characterIndex != this->beamLocation) {
  258. this->selectionStart = newBeam.characterIndex;
  259. this->beamLocation = newBeam.characterIndex;
  260. this->hasImages = false;
  261. }
  262. } else if (this->mousePressed && event.mouseEventType == MouseEventType::MouseMove) {
  263. if (this->mousePressed) {
  264. BeamLocation newBeam = findBeamLocation(LVector2D(event.position.x - this->location.left(), event.position.y - this->location.top()));
  265. if (newBeam.characterIndex != this->beamLocation) {
  266. this->beamLocation = newBeam.characterIndex;
  267. this->hasImages = false;
  268. }
  269. }
  270. } else if (this->mousePressed && event.mouseEventType == MouseEventType::MouseUp) {
  271. this->mousePressed = false;
  272. }
  273. if (scrollIntercepted) {
  274. this->hasImages = false; // Force redraw on scrollbar interception
  275. } else {
  276. VisualComponent::receiveMouseEvent(event);
  277. }
  278. }
  279. ReadableString TextBox::getSelectedText() {
  280. int64_t selectionLeft = std::min(this->selectionStart, this->beamLocation);
  281. int64_t selectionRight = std::max(this->selectionStart, this->beamLocation);
  282. return string_exclusiveRange(this->text.value, selectionLeft, selectionRight);
  283. }
  284. void TextBox::replaceSelection(const ReadableString &replacingText) {
  285. int64_t selectionLeft = std::min(this->selectionStart, this->beamLocation);
  286. int64_t selectionRight = std::max(this->selectionStart, this->beamLocation);
  287. this->text.value = string_combine(string_before(this->text.value, selectionLeft), replacingText, string_from(this->text.value, selectionRight));
  288. // Place beam on the right side of the replacement without selecting anything
  289. this->selectionStart = selectionLeft + string_length(replacingText);
  290. this->beamLocation = selectionStart;
  291. this->hasImages = false;
  292. this->indexedAtLength = -1;
  293. this->indexLines();
  294. this->limitScrolling(true);
  295. }
  296. void TextBox::replaceSelection(DsrChar replacingCharacter) {
  297. int64_t selectionLeft = std::min(this->selectionStart, this->beamLocation);
  298. int64_t selectionRight = std::max(this->selectionStart, this->beamLocation);
  299. String newText = string_before(this->text.value, selectionLeft);
  300. string_appendChar(newText, replacingCharacter);
  301. string_append(newText, string_from(this->text.value, selectionRight));
  302. this->text.value = newText;
  303. // Place beam on the right side of the replacement without selecting anything
  304. this->selectionStart = selectionLeft + 1;
  305. this->beamLocation = selectionStart;
  306. this->hasImages = false;
  307. this->indexedAtLength = -1;
  308. this->indexLines();
  309. this->limitScrolling(true);
  310. }
  311. void TextBox::placeBeamAtCharacter(int64_t characterIndex, bool removeSelection) {
  312. this->beamLocation = characterIndex;
  313. if (removeSelection) {
  314. this->selectionStart = characterIndex;
  315. }
  316. this->hasImages = false;
  317. this->limitScrolling(true);
  318. }
  319. void TextBox::moveBeamVertically(int64_t rowIndexOffset, bool removeSelection) {
  320. // Find the current beam's row index.
  321. int64_t oldRowIndex = findBeamRow(this->lines, this->beamLocation);
  322. // Find another row.
  323. int64_t newRowIndex = oldRowIndex + rowIndexOffset;
  324. if (newRowIndex < 0) { newRowIndex = 0; }
  325. if (newRowIndex >= this->lines.length()) { newRowIndex = this->lines.length() - 1; }
  326. // Get old pixel offset from the beam.
  327. LVector2D origin = this->getTextOrigin(true);
  328. BeamLocation oldBeam = BeamLocation(oldRowIndex, this->beamLocation);
  329. int64_t oldPixelOffset = origin.x + getBeamPixelOffset(this->text.value, this->font, this->lines, oldBeam);
  330. // Get the closest location in the new row.
  331. int64_t newCharacterIndex = findBeamLocationInLine(newRowIndex, oldPixelOffset);
  332. placeBeamAtCharacter(newCharacterIndex, removeSelection);
  333. limitScrolling(true);
  334. }
  335. static const uint32_t combinationKey_shift = 1 << 0;
  336. static const uint32_t combinationKey_control = 1 << 1;
  337. static int64_t getLineStart(const ReadableString &text, int64_t searchStart) {
  338. for (int64_t i = searchStart - 1; i >= 0; i--) {
  339. if (text[i] == U'\n') {
  340. return i + 1;
  341. }
  342. }
  343. return 0;
  344. }
  345. static int64_t getLineEnd(const ReadableString &text, int64_t searchStart) {
  346. for (int64_t i = searchStart; i < string_length(text); i++) {
  347. if (text[i] == U'\n') {
  348. return i;
  349. }
  350. }
  351. return string_length(text);
  352. }
  353. // TODO: Use DsrKey_PageUp and DsrKey_PageDown.
  354. void TextBox::receiveKeyboardEvent(const KeyboardEvent& event) {
  355. // Insert and scroll-lock is not supported.
  356. // To prevent getting stuck from missing a key event, one can reset by pressing and releasing again.
  357. // So if you press down both control keys and release one of them, it counts both as released.
  358. if (event.keyboardEventType == KeyboardEventType::KeyDown) {
  359. if (event.dsrKey == DsrKey_Shift) {
  360. this->combinationKeys |= combinationKey_shift; // Enable shift
  361. } else if (event.dsrKey == DsrKey_Control) {
  362. this->combinationKeys |= combinationKey_control; // Enable control
  363. }
  364. } else if (event.keyboardEventType == KeyboardEventType::KeyUp) {
  365. if (event.dsrKey == DsrKey_Shift) {
  366. this->combinationKeys &= ~combinationKey_shift; // Disable shift
  367. } else if (event.dsrKey == DsrKey_Control) {
  368. this->combinationKeys &= ~combinationKey_control; // Disable control
  369. }
  370. } else if (event.keyboardEventType == KeyboardEventType::KeyType) {
  371. int64_t textLength = string_length(this->text.value);
  372. bool selected = this->selectionStart != this->beamLocation;
  373. bool printable = event.character == U'\t' || (31 < event.character && event.character < 127) || 159 < event.character;
  374. bool canGoLeft = textLength > 0 && this->beamLocation > 0;
  375. bool canGoRight = textLength > 0 && this->beamLocation < textLength;
  376. bool holdShift = this->combinationKeys & combinationKey_shift;
  377. bool holdControl = this->combinationKeys & combinationKey_control;
  378. bool removeSelection = !holdShift;
  379. if (holdControl) {
  380. if (event.dsrKey == DsrKey_LeftArrow) {
  381. // Move to the line start using Ctrl + LeftArrow instead of Home
  382. this->placeBeamAtCharacter(getLineStart(this->text.value, this->beamLocation), removeSelection);
  383. } else if (event.dsrKey == DsrKey_RightArrow) {
  384. // Move to the line end using Ctrl + RightArrow instead of End
  385. this->placeBeamAtCharacter(getLineEnd(this->text.value, this->beamLocation), removeSelection);
  386. } else if (event.dsrKey == DsrKey_X) {
  387. // Cut selection using Ctrl + X
  388. if (this->window.getUnsafe()) {
  389. this->window->saveToClipboard(this->getSelectedText());
  390. this->replaceSelection(U"");
  391. } else {
  392. sendWarning(U"No window handle found in TextBox when trying to cut text!");
  393. }
  394. } else if (event.dsrKey == DsrKey_C) {
  395. // Copy selection using Ctrl + C
  396. if (this->window.getUnsafe()) {
  397. this->window->saveToClipboard(this->getSelectedText());
  398. } else {
  399. sendWarning(U"No window handle found in TextBox when trying to copy text!");
  400. }
  401. } else if (event.dsrKey == DsrKey_V) {
  402. // Paste selection using Ctrl + V
  403. if (this->window.getUnsafe()) {
  404. this->replaceSelection(this->window->loadFromClipboard());
  405. } else {
  406. sendWarning(U"No window handle found in TextBox when trying to paste text!");
  407. }
  408. } else if (event.dsrKey == DsrKey_A) {
  409. // Select all using Ctrl + A
  410. this->selectionStart = 0;
  411. this->beamLocation = string_length(this->text.value);
  412. this->hasImages = false;
  413. } else if (event.dsrKey == DsrKey_N) {
  414. // Select nothing using Ctrl + N
  415. this->selectionStart = this->beamLocation;
  416. this->hasImages = false;
  417. }
  418. } else {
  419. if (selected && (event.dsrKey == DsrKey_BackSpace || event.dsrKey == DsrKey_Delete)) {
  420. // Remove selection
  421. this->replaceSelection(U"");
  422. } else if (event.dsrKey == DsrKey_BackSpace) {
  423. if (this->selectionStart == this->beamLocation) {
  424. if (this->beamLocation > 0) {
  425. // Erase left of beam
  426. this->beamLocation--;
  427. this->replaceSelection(U"");
  428. }
  429. } else {
  430. // Erase selection
  431. this->replaceSelection(U"");
  432. }
  433. } else if (event.dsrKey == DsrKey_Delete) {
  434. if (this->selectionStart == this->beamLocation) {
  435. if (this->beamLocation < textLength) {
  436. // Erase right of beam
  437. this->beamLocation++;
  438. this->replaceSelection(U"");
  439. }
  440. } else {
  441. // Erase selection
  442. this->replaceSelection(U"");
  443. }
  444. } else if (event.dsrKey == DsrKey_Home) {
  445. // Move to the line start using Home
  446. this->placeBeamAtCharacter(getLineStart(this->text.value, this->beamLocation), removeSelection);
  447. } else if (event.dsrKey == DsrKey_End) {
  448. // Move to the line end using End
  449. this->placeBeamAtCharacter(getLineEnd(this->text.value, this->beamLocation), removeSelection);
  450. } else if (event.dsrKey == DsrKey_LeftArrow && canGoLeft) {
  451. // Move left using LeftArrow
  452. this->placeBeamAtCharacter(this->beamLocation - 1, removeSelection);
  453. } else if (event.dsrKey == DsrKey_RightArrow && canGoRight) {
  454. // Move right using RightArrow
  455. this->placeBeamAtCharacter(this->beamLocation + 1, removeSelection);
  456. } else if (event.dsrKey == DsrKey_UpArrow) {
  457. // Move up using UpArrow
  458. this->moveBeamVertically(-1, removeSelection);
  459. } else if (event.dsrKey == DsrKey_DownArrow) {
  460. // Move down using DownArrow
  461. this->moveBeamVertically(1, removeSelection);
  462. } else if (event.dsrKey == DsrKey_Return) {
  463. if (this->multiLine.value) {
  464. this->replaceSelection(U'\n');
  465. }
  466. } else if (printable) {
  467. this->replaceSelection(event.character);
  468. }
  469. }
  470. //printText(U"KeyType char=", event.character, " key=", event.dsrKey, U"\n");
  471. }
  472. VisualComponent::receiveKeyboardEvent(event);
  473. }
  474. bool TextBox::pointIsInside(const IVector2D& pixelPosition) {
  475. this->generateGraphics();
  476. // Get the point relative to the component instead of its direct container
  477. IVector2D localPoint = pixelPosition - this->location.upperLeft();
  478. // Sample opacity at the location
  479. return dsr::image_readPixel_border(this->image, localPoint.x, localPoint.y).alpha > 127;
  480. }
  481. void TextBox::loadTheme(const VisualTheme &theme) {
  482. this->finalBackgroundClass = theme_selectClass(theme, this->backgroundClass.value, U"TextBox");
  483. this->textBox = theme_getScalableImage(theme, this->finalBackgroundClass);
  484. this->verticalScrollBar.loadTheme(theme, this->backColor.value);
  485. this->horizontalScrollBar.loadTheme(theme, this->backColor.value);
  486. this->background_filter = theme_getInteger(theme, this->finalBackgroundClass, U"Filter", 0);
  487. }
  488. void TextBox::changedTheme(VisualTheme newTheme) {
  489. this->loadTheme(newTheme);
  490. this->hasImages = false;
  491. }
  492. void TextBox::loadFont() {
  493. if (!font_exists(this->font)) {
  494. this->font = font_getDefault();
  495. }
  496. if (!font_exists(this->font)) {
  497. throwError("Failed to load the default font for a ListBox!\n");
  498. }
  499. }
  500. void TextBox::completeAssets() {
  501. if (this->textBox.methodIndex == -1) {
  502. this->loadTheme(theme_getDefault());
  503. }
  504. this->loadFont();
  505. }
  506. void TextBox::changedLocation(const IRect &oldLocation, const IRect &newLocation) {
  507. // If the component has changed dimensions then redraw the image
  508. if (oldLocation.size() != newLocation.size()) {
  509. this->hasImages = false;
  510. this->limitScrolling(true);
  511. }
  512. }
  513. void TextBox::changedAttribute(const ReadableString &name) {
  514. if (string_caseInsensitiveMatch(name, U"BackgroundClass")) {
  515. // Update from the theme if the theme class has changed.
  516. this->changedTheme(this->getTheme());
  517. } else if (!string_caseInsensitiveMatch(name, U"Visible")) {
  518. this->hasImages = false;
  519. if (string_caseInsensitiveMatch(name, U"Text")) {
  520. this->indexedAtLength = -1;
  521. this->limitSelection();
  522. this->limitScrolling(true);
  523. }
  524. }
  525. VisualComponent::changedAttribute(name);
  526. }
  527. void TextBox::updateScrollRange() {
  528. this->loadFont();
  529. // How high is one element?
  530. int64_t verticalStep = font_getSize(this->font);
  531. // How many elements are visible at the same time?
  532. int64_t visibleRangeY = (this->location.height() - this->borderY * 2) / verticalStep;
  533. if (visibleRangeY < 1) visibleRangeY = 1;
  534. // How many lines are there in total to see.
  535. int64_t itemCount = this->lines.length() + 1; // Reserve an extra line for the horizontal scroll-bar.
  536. // The range of indices that the listbox can start viewing from.
  537. int64_t maxScrollY = itemCount - visibleRangeY;
  538. // If visible range exceeds the collection, we should still allow starting element zero to get a valid range.
  539. if (maxScrollY < 0) maxScrollY = 0;
  540. // Apply the scroll range.
  541. this->verticalScrollBar.updateScrollRange(ScrollRange(0, maxScrollY, visibleRangeY));
  542. // Calculate range for horizontal scroll.
  543. int64_t monospaceWidth = font_getMonospaceWidth(this->font);
  544. int64_t rightMostPixel = this->worstCaseLineMonospaces * monospaceWidth;
  545. int64_t visibleRangeX = this->location.width() - this->borderX * 2;
  546. if (visibleRangeX < 1) visibleRangeX = 1;
  547. int64_t maxScrollX = rightMostPixel; // Allow scrolling all the way out, so that one can write left to right without constantly panorating on a long line.
  548. if (maxScrollX < 0) maxScrollX = 0;
  549. this->horizontalScrollBar.updateScrollRange(ScrollRange(0, maxScrollX, visibleRangeX));
  550. }
  551. void TextBox::limitScrolling(bool keepBeamVisible) {
  552. // Update the scroll range.
  553. this->indexLines();
  554. this->updateScrollRange();
  555. // Limit scrolling with the updated range.
  556. if (keepBeamVisible) {
  557. int64_t beamRow = findBeamRow(this->lines, this->beamLocation);
  558. BeamLocation beam = BeamLocation(beamRow, this->beamLocation);
  559. // What will origin.x be used for?
  560. int64_t pixelOffsetX = getBeamPixelOffset(this->text.value, this->font, this->lines, beam);
  561. this->verticalScrollBar.limitScrolling(this->location, true, beamRow);
  562. this->horizontalScrollBar.limitScrolling(this->location, true, pixelOffsetX);
  563. } else {
  564. this->verticalScrollBar.limitScrolling(this->location);
  565. this->horizontalScrollBar.limitScrolling(this->location);
  566. }
  567. }