TextInput.hx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. package h2d;
  2. import hxd.Key in K;
  3. private typedef TextHistoryElement = { t : String, c : Int, sel : { start : Int, length : Int } };
  4. /**
  5. A skinnable text input handler.
  6. Supports text selection, keyboard cursor navigation, as well as basic hotkeys: `Ctrl + Z`, `Ctrl + Y` for undo and redo and `Ctrl + A` to select all text.
  7. **/
  8. class TextInput extends Text {
  9. /**
  10. Current position of the input cursor.
  11. When TextInput is not focused value is -1.
  12. **/
  13. public var cursorIndex : Int = -1;
  14. /**
  15. The Tile used to render the input cursor.
  16. **/
  17. public var cursorTile : h2d.Tile;
  18. /**
  19. The Tile used to render the background for selected text.
  20. When rendering, this Tile is stretched horizontally to fill entire selection area.
  21. **/
  22. public var selectionTile : h2d.Tile;
  23. /**
  24. The blinking interval of the cursor in seconds.
  25. **/
  26. public var cursorBlinkTime = 0.5;
  27. /**
  28. Maximum input width.
  29. Contrary to `Text.maxWidth` does not cause a word-wrap, but also masks out contents that are outside the max width.
  30. **/
  31. public var inputWidth : Null<Int>;
  32. /**
  33. Whether the text input allows multiple lines.
  34. **/
  35. public var multiline: Bool = false;
  36. /**
  37. If not null, represents current text selection range.
  38. **/
  39. public var selectionRange : { start : Int, length : Int };
  40. /**
  41. When disabled, user would not be able to edit the input text (selection is still available).
  42. **/
  43. public var canEdit = true;
  44. /**
  45. If set, TextInput will render provided color as a background to text interactive area.
  46. **/
  47. public var backgroundColor(get, set) : Null<Int>;
  48. /**
  49. When disabled, showSoftwareKeyboard will not be called.
  50. **/
  51. public var useSoftwareKeyboard : Bool = true;
  52. public static dynamic function showSoftwareKeyboard(target:TextInput) {}
  53. public static dynamic function hideSoftwareKeyboard(target:TextInput) {}
  54. var interactive : h2d.Interactive;
  55. var cursorText : String;
  56. var cursorX : Float;
  57. var cursorXIndex : Int;
  58. var cursorY : Float;
  59. var cursorYIndex : Int;
  60. var cursorBlink = 0.;
  61. var cursorScroll = 0;
  62. var scrollX = 0.;
  63. var selectionPos : Float;
  64. var selectionSize : Float;
  65. var undo : Array<TextHistoryElement> = [];
  66. var redo : Array<TextHistoryElement> = [];
  67. var lastChange = 0.;
  68. var lastClick = 0.;
  69. var maxHistorySize = 100;
  70. /**
  71. Create a new TextInput instance.
  72. @param font The font used to render the text.
  73. @param parent An optional parent `h2d.Object` instance to which TextInput adds itself if set.
  74. **/
  75. public function new(font, ?parent) {
  76. super(font, parent);
  77. interactive = new h2d.Interactive(0, 0);
  78. interactive.cursor = TextInput;
  79. interactive.onPush = function(e:hxd.Event) {
  80. onPush(e);
  81. if( !e.cancel && e.button == 0 ) {
  82. if( !interactive.hasFocus() ) {
  83. e.kind = EFocus;
  84. onFocus(e);
  85. e.kind = EPush;
  86. if( e.cancel ) return;
  87. interactive.focus();
  88. }
  89. cursorBlink = 0;
  90. var startIndex = textPos(e.relX, e.relY);
  91. cursorIndex = startIndex;
  92. selectionRange = null;
  93. var pt = new h2d.col.Point();
  94. var scene = getScene();
  95. if( scene == null ) return; // was removed
  96. scene.startCapture(function(e) {
  97. pt.x = e.relX;
  98. pt.y = e.relY;
  99. globalToLocal(pt);
  100. var index = textPos(pt.x, pt.y);
  101. if( index == startIndex )
  102. selectionRange = null;
  103. else if( index < startIndex )
  104. selectionRange = { start : index, length : startIndex - index };
  105. else
  106. selectionRange = { start : startIndex, length : index - startIndex };
  107. selectionSize = 0;
  108. cursorIndex = index;
  109. if( e.kind == ERelease || getScene() != scene )
  110. scene.stopCapture();
  111. });
  112. }
  113. };
  114. interactive.onKeyDown = function(e:hxd.Event) {
  115. onKeyDown(e);
  116. handleKey(e);
  117. };
  118. interactive.onTextInput = function(e:hxd.Event) {
  119. onTextInput(e);
  120. handleKey(e);
  121. };
  122. interactive.onFocus = function(e) {
  123. onFocus(e);
  124. if ( useSoftwareKeyboard && canEdit )
  125. showSoftwareKeyboard(this);
  126. }
  127. interactive.onFocusLost = function(e) {
  128. cursorIndex = -1;
  129. selectionRange = null;
  130. hideSoftwareKeyboard(this);
  131. onFocusLost(e);
  132. };
  133. interactive.onClick = function(e) {
  134. onClick(e);
  135. if( e.cancel ) return;
  136. var t = haxe.Timer.stamp();
  137. // double click to select all
  138. if( t - lastClick < 0.3 && text.length != 0 ) {
  139. selectionRange = { start : 0, length : text.length };
  140. selectionSize = 0;
  141. cursorIndex = text.length;
  142. }
  143. lastClick = t;
  144. };
  145. interactive.onKeyUp = function(e) onKeyUp(e);
  146. interactive.onRelease = function(e) onRelease(e);
  147. interactive.onKeyUp = function(e) onKeyUp(e);
  148. interactive.onMove = function(e) onMove(e);
  149. interactive.onOver = function(e) onOver(e);
  150. interactive.onOut = function(e) onOut(e);
  151. interactive.cursor = TextInput;
  152. addChildAt(interactive, 0);
  153. }
  154. override function constraintSize(width:Float, height:Float) {
  155. // disable (don't allow multiline textinput for now)
  156. }
  157. function handleKey( e : hxd.Event ) {
  158. if( e.cancel || cursorIndex < 0 )
  159. return;
  160. var oldIndex = cursorIndex;
  161. var oldText = text;
  162. switch( e.keyCode ) {
  163. case K.LEFT if (K.isDown(K.CTRL)):
  164. cursorIndex = getWordStart();
  165. case K.LEFT:
  166. if( cursorIndex > 0 )
  167. cursorIndex--;
  168. case K.RIGHT if (K.isDown(K.CTRL)):
  169. cursorIndex = getWordEnd();
  170. case K.RIGHT:
  171. if( cursorIndex < text.length )
  172. cursorIndex++;
  173. case K.HOME:
  174. cursorIndex = 0;
  175. case K.END:
  176. cursorIndex = text.length;
  177. case K.BACKSPACE, K.DELETE if( selectionRange != null ):
  178. if( !canEdit ) return;
  179. beforeChange();
  180. cutSelection();
  181. onChange();
  182. case K.DELETE:
  183. if( cursorIndex < text.length && canEdit ) {
  184. beforeChange();
  185. var end = K.isDown(K.CTRL) ? getWordEnd() : cursorIndex + 1;
  186. text = text.substr(0, cursorIndex) + text.substr(end);
  187. onChange();
  188. }
  189. case K.BACKSPACE:
  190. if( cursorIndex > 0 && canEdit ) {
  191. beforeChange();
  192. var end = cursorIndex;
  193. cursorIndex = K.isDown(K.CTRL) ? getWordStart() : cursorIndex - 1;
  194. text = text.substr(0, cursorIndex) + text.substr(end);
  195. onChange();
  196. }
  197. case K.ESCAPE:
  198. cursorIndex = -1;
  199. interactive.blur();
  200. return;
  201. case K.ENTER, K.NUMPAD_ENTER:
  202. if(!multiline) {
  203. cursorIndex = -1;
  204. interactive.blur();
  205. return;
  206. } else {
  207. beforeChange();
  208. if( selectionRange != null )
  209. cutSelection();
  210. text = text.substr(0, cursorIndex) + '\n' + text.substr(cursorIndex);
  211. cursorIndex++;
  212. onChange();
  213. }
  214. case K.Z if( K.isDown(K.CTRL) ):
  215. if( undo.length > 0 && canEdit ) {
  216. redo.push(curHistoryState());
  217. setState(undo.pop());
  218. onChange();
  219. }
  220. return;
  221. case K.Y if( K.isDown(K.CTRL) ):
  222. if( redo.length > 0 && canEdit ) {
  223. undo.push(curHistoryState());
  224. setState(redo.pop());
  225. onChange();
  226. }
  227. return;
  228. case K.A if (K.isDown(K.CTRL)):
  229. if (text != "") {
  230. cursorIndex = text.length;
  231. selectionRange = {start: 0, length: text.length};
  232. selectionSize = 0;
  233. }
  234. return;
  235. case K.C if (K.isDown(K.CTRL)):
  236. if( text != "" && selectionRange != null ) {
  237. hxd.System.setClipboardText(text.substr(selectionRange.start, selectionRange.length));
  238. }
  239. case K.X if (K.isDown(K.CTRL)):
  240. if( text != "" && selectionRange != null ) {
  241. if(hxd.System.setClipboardText(text.substr(selectionRange.start, selectionRange.length))) {
  242. if( !canEdit ) return;
  243. beforeChange();
  244. cutSelection();
  245. onChange();
  246. }
  247. }
  248. case K.V if (K.isDown(K.CTRL)):
  249. if( !canEdit ) return;
  250. var t = hxd.System.getClipboardText();
  251. if( t != null && t.length > 0 ) {
  252. beforeChange();
  253. if( selectionRange != null )
  254. cutSelection();
  255. text = text.substr(0, cursorIndex) + t + text.substr(cursorIndex);
  256. cursorIndex += t.length;
  257. onChange();
  258. }
  259. default:
  260. if( e.kind == EKeyDown )
  261. return;
  262. if( e.charCode != 0 && canEdit ) {
  263. if( !font.hasChar(e.charCode) ) return; // don't allow chars not supported by font
  264. beforeChange();
  265. if( selectionRange != null )
  266. cutSelection();
  267. text = text.substr(0, cursorIndex) + String.fromCharCode(e.charCode) + text.substr(cursorIndex);
  268. cursorIndex++;
  269. onChange();
  270. }
  271. }
  272. cursorBlink = 0.;
  273. if( K.isDown(K.SHIFT) && text == oldText ) {
  274. if( cursorIndex == oldIndex ) return;
  275. if( selectionRange == null )
  276. selectionRange = oldIndex < cursorIndex ? { start : oldIndex, length : cursorIndex - oldIndex } : { start : cursorIndex, length : oldIndex - cursorIndex };
  277. else if( oldIndex == selectionRange.start ) {
  278. selectionRange.length += oldIndex - cursorIndex;
  279. selectionRange.start = cursorIndex;
  280. } else
  281. selectionRange.length += cursorIndex - oldIndex;
  282. if( selectionRange.length == 0 )
  283. selectionRange = null;
  284. else if( selectionRange.length < 0 ) {
  285. selectionRange.start += selectionRange.length;
  286. selectionRange.length = -selectionRange.length;
  287. }
  288. selectionSize = 0;
  289. } else
  290. selectionRange = null;
  291. }
  292. function cutSelection() {
  293. if(selectionRange == null) return false;
  294. cursorIndex = selectionRange.start;
  295. var end = cursorIndex + selectionRange.length;
  296. text = text.substr(0, cursorIndex) + text.substr(end);
  297. selectionRange = null;
  298. return true;
  299. }
  300. function getWordEnd() {
  301. var len = text.length;
  302. if (cursorIndex >= len) {
  303. return cursorIndex;
  304. }
  305. var charset = hxd.Charset.getDefault();
  306. var ret = cursorIndex;
  307. while (ret < len && charset.isSpace(StringTools.fastCodeAt(text, ret))) ret++;
  308. while (ret < len && !charset.isSpace(StringTools.fastCodeAt(text, ret))) ret++;
  309. return ret;
  310. }
  311. function getWordStart() {
  312. if (cursorIndex <= 0) {
  313. return cursorIndex;
  314. }
  315. var charset = hxd.Charset.getDefault();
  316. var ret = cursorIndex;
  317. while (ret > 0 && charset.isSpace(StringTools.fastCodeAt(text, ret - 1))) ret--;
  318. while (ret > 0 && !charset.isSpace(StringTools.fastCodeAt(text, ret - 1))) ret--;
  319. return ret;
  320. }
  321. function setState(h:TextHistoryElement) {
  322. text = h.t;
  323. cursorIndex = h.c;
  324. selectionRange = h.sel;
  325. if( selectionRange != null )
  326. cursorIndex = selectionRange.start + selectionRange.length;
  327. }
  328. function curHistoryState() : TextHistoryElement {
  329. return { t : text, c : cursorIndex, sel : selectionRange == null ? null : { start : selectionRange.start, length : selectionRange.length } };
  330. }
  331. function beforeChange() {
  332. var t = haxe.Timer.stamp();
  333. if( t - lastChange < 1 ) {
  334. lastChange = t;
  335. return;
  336. }
  337. lastChange = t;
  338. undo.push(curHistoryState());
  339. redo = [];
  340. while( undo.length > maxHistorySize ) undo.shift();
  341. }
  342. function getAllLines() {
  343. var lines = this.text.split('\n');
  344. var finalLines : Array<String> = [];
  345. for(l in lines) {
  346. var splitText = splitText(l).split('\n');
  347. finalLines = finalLines.concat(splitText);
  348. }
  349. for(i in 0...finalLines.length) {
  350. finalLines[i] += '\n';
  351. }
  352. return finalLines;
  353. }
  354. function getCurrentLine() : String {
  355. var lines = getAllLines();
  356. var currIndex = 0;
  357. for(i in 0...lines.length) {
  358. currIndex += lines[i].length;
  359. if(cursorIndex < currIndex) {
  360. return lines[i];
  361. }
  362. }
  363. return '';
  364. }
  365. function getCursorXOffset() {
  366. var lines = getAllLines();
  367. var offset = cursorIndex;
  368. var currLine = getCurrentLine();
  369. var currIndex = 0;
  370. for(i in 0...lines.length) {
  371. currIndex += lines[i].length;
  372. if(cursorIndex < currIndex) {
  373. break;
  374. } else {
  375. offset -= lines[i].length;
  376. }
  377. }
  378. return calcTextWidth(currLine.substr(0, offset));
  379. }
  380. function getCursorYOffset() {
  381. // return 0.0;
  382. var lines = getAllLines();
  383. var currIndex = 0;
  384. var lineNum = 0;
  385. for(i in 0...lines.length) {
  386. currIndex += lines[i].length;
  387. if(cursorIndex < currIndex) {
  388. lineNum = i;
  389. break;
  390. }
  391. }
  392. return lineNum * font.lineHeight;
  393. }
  394. /**
  395. Returns a String representing currently selected text area or `null` if no text is selected.
  396. **/
  397. public function getSelectedText() : String {
  398. return selectionRange == null ? null : text.substr(selectionRange.start, selectionRange.length);
  399. }
  400. override function set_text(t:String) {
  401. super.set_text(t);
  402. if( cursorIndex > t.length ) cursorIndex = t.length;
  403. return t;
  404. }
  405. override function set_font(f) {
  406. super.set_font(f);
  407. cursorTile = h2d.Tile.fromColor(0xFFFFFF, 1, font.size);
  408. cursorTile.dy = 2;
  409. selectionTile = h2d.Tile.fromColor(0x3399FF, 0, hxd.Math.ceil(font.lineHeight));
  410. return f;
  411. }
  412. override function initGlyphs(text:String, rebuild = true):Void {
  413. super.initGlyphs(text, rebuild);
  414. if( rebuild ) {
  415. this.calcWidth += cursorTile.width; // cursor end pos
  416. if( inputWidth != null && this.calcWidth > inputWidth ) this.calcWidth = inputWidth;
  417. }
  418. }
  419. function textPos( x : Float, y : Float ) {
  420. x += scrollX;
  421. var lineIndex = Math.floor(y / font.lineHeight);
  422. var lines = getAllLines();
  423. lineIndex = hxd.Math.iclamp(lineIndex, 0, lines.length - 1);
  424. var selectedLine = lines[lineIndex];
  425. var pos = 0;
  426. for(i in 0...lineIndex) {
  427. pos += lines[i].length;
  428. }
  429. var linePos = 0;
  430. while( linePos < selectedLine.length ) {
  431. if( calcTextWidth(selectedLine.substr(0,linePos+1)) > x ) {
  432. pos++;
  433. break;
  434. }
  435. pos++;
  436. linePos++;
  437. }
  438. return pos - 1;
  439. }
  440. override function sync(ctx) {
  441. var lines = getAllLines();
  442. interactive.width = (inputWidth != null ? inputWidth : maxWidth != null ? Math.ceil(maxWidth) : textWidth);
  443. interactive.height = font.lineHeight * lines.length;
  444. super.sync(ctx);
  445. }
  446. override function draw(ctx:RenderContext) {
  447. if( inputWidth != null ) {
  448. var h = localToGlobal(new h2d.col.Point(inputWidth, font.lineHeight));
  449. ctx.clipRenderZone(absX, absY, h.x - absX, h.y - absY);
  450. }
  451. if( cursorIndex >= 0 && (text != cursorText || cursorIndex != cursorXIndex) ) {
  452. if( cursorIndex > text.length ) cursorIndex = text.length;
  453. cursorText = text;
  454. cursorXIndex = cursorIndex;
  455. cursorX = getCursorXOffset();
  456. cursorY = getCursorYOffset();
  457. if( inputWidth != null && cursorX - scrollX >= inputWidth )
  458. scrollX = cursorX - inputWidth + 1;
  459. else if( cursorX < scrollX && cursorIndex > 0 )
  460. scrollX = cursorX - hxd.Math.imin(inputWidth, Std.int(cursorX));
  461. else if( cursorX < scrollX )
  462. scrollX = cursorX;
  463. }
  464. absX -= scrollX * matA;
  465. absY -= scrollX * matC;
  466. if( selectionRange != null ) {
  467. var lines = getAllLines();
  468. var lineOffset = 0;
  469. for(i in 0...lines.length) {
  470. var line = lines[i];
  471. var selEnd = line.length;
  472. if(selectionRange.start > lineOffset + line.length || selectionRange.start + selectionRange.length < lineOffset) {
  473. lineOffset += line.length;
  474. continue;
  475. }
  476. var selStart = Math.floor(Math.max(0, selectionRange.start - lineOffset));
  477. var selEnd = Math.floor(Math.min(line.length - selStart, selectionRange.length + selectionRange.start - lineOffset - selStart));
  478. selectionPos = calcTextWidth(line.substr(0, selStart));
  479. selectionSize = calcTextWidth(line.substr(selStart, selEnd));
  480. if( selectionRange.start + selectionRange.length == text.length ) selectionSize += cursorTile.width; // last pixel
  481. selectionTile.dx += selectionPos;
  482. selectionTile.dy += i * font.lineHeight;
  483. selectionTile.width += selectionSize;
  484. emitTile(ctx, selectionTile);
  485. selectionTile.dx -= selectionPos;
  486. selectionTile.dy -= i * font.lineHeight;
  487. selectionTile.width -= selectionSize;
  488. lineOffset += line.length;
  489. }
  490. }
  491. super.draw(ctx);
  492. absX += scrollX * matA;
  493. absY += scrollX * matC;
  494. if( cursorIndex >= 0 ) {
  495. cursorBlink += ctx.elapsedTime;
  496. if( cursorBlink % (cursorBlinkTime * 2) < cursorBlinkTime ) {
  497. cursorTile.dx += cursorX - scrollX;
  498. cursorTile.dy += cursorY;
  499. emitTile(ctx, cursorTile);
  500. cursorTile.dx -= cursorX - scrollX;
  501. cursorTile.dy -= cursorY;
  502. }
  503. }
  504. if( inputWidth != null )
  505. ctx.popRenderZone();
  506. }
  507. /**
  508. Sets focus on this `TextInput`.
  509. **/
  510. public function focus() {
  511. interactive.focus();
  512. if( cursorIndex < 0 ) {
  513. cursorIndex = 0;
  514. if( text != "" ) selectionRange = { start : 0, length : text.length };
  515. }
  516. }
  517. /**
  518. Checks if TextInput is currently focused.
  519. **/
  520. public function hasFocus() {
  521. return interactive.hasFocus();
  522. }
  523. /**
  524. Delegate of underlying `Interactive.onOut`.
  525. **/
  526. public dynamic function onOut(e:hxd.Event) {
  527. }
  528. /**
  529. Delegate of underlying `Interactive.onOver`.
  530. **/
  531. public dynamic function onOver(e:hxd.Event) {
  532. }
  533. /**
  534. Delegate of underlying `Interactive.onMove`.
  535. **/
  536. public dynamic function onMove(e:hxd.Event) {
  537. }
  538. /**
  539. Delegate of underlying `Interactive.onClick`.
  540. **/
  541. public dynamic function onClick(e:hxd.Event) {
  542. }
  543. /**
  544. Delegate of underlying `Interactive.onPush`.
  545. **/
  546. public dynamic function onPush(e:hxd.Event) {
  547. }
  548. /**
  549. Delegate of underlying `Interactive.onRelease`.
  550. **/
  551. public dynamic function onRelease(e:hxd.Event) {
  552. }
  553. /**
  554. Delegate of underlying `Interactive.onKeyDown`.
  555. **/
  556. public dynamic function onKeyDown(e:hxd.Event) {
  557. }
  558. /**
  559. Delegate of underlying `Interactive.onKeyUp`.
  560. **/
  561. public dynamic function onKeyUp(e:hxd.Event) {
  562. }
  563. /**
  564. Delegate of underlying `Interactive.onTextInput`.
  565. **/
  566. public dynamic function onTextInput(e:hxd.Event) {
  567. }
  568. /**
  569. Delegate of underlying `Interactive.onFocus`.
  570. **/
  571. public dynamic function onFocus(e:hxd.Event) {
  572. }
  573. /**
  574. Delegate of underlying `Interactive.onFocusLost`.
  575. **/
  576. public dynamic function onFocusLost(e:hxd.Event) {
  577. }
  578. /**
  579. Sent when user modifies TextInput contents.
  580. **/
  581. public dynamic function onChange() {
  582. }
  583. override function drawRec(ctx:RenderContext) {
  584. var old = interactive.visible;
  585. interactive.visible = false;
  586. interactive.draw(ctx);
  587. super.drawRec(ctx);
  588. interactive.visible = old;
  589. }
  590. function get_backgroundColor() return interactive.backgroundColor;
  591. function set_backgroundColor(v) return interactive.backgroundColor = v;
  592. }