From cce7231b11aef87bfd6b37b922eb459b4db85922 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Tue, 18 Jun 2024 21:59:41 -0400 Subject: [PATCH 01/14] Initial text input implementation --- flixel/FlxG.hx | 13 +- flixel/system/frontEnds/InputTextFrontEnd.hx | 204 +++++++ flixel/text/FlxInputText.hx | 579 +++++++++++++++++++ 3 files changed, 791 insertions(+), 5 deletions(-) create mode 100644 flixel/system/frontEnds/InputTextFrontEnd.hx create mode 100644 flixel/text/FlxInputText.hx diff --git a/flixel/FlxG.hx b/flixel/FlxG.hx index e855617a4c..22b8daf8a8 100644 --- a/flixel/FlxG.hx +++ b/flixel/FlxG.hx @@ -1,10 +1,5 @@ package flixel; -import openfl.Lib; -import openfl.display.DisplayObject; -import openfl.display.Stage; -import openfl.display.StageDisplayState; -import openfl.net.URLRequest; import flixel.effects.postprocess.PostProcess; import flixel.math.FlxMath; import flixel.math.FlxRandom; @@ -17,6 +12,7 @@ import flixel.system.frontEnds.CameraFrontEnd; import flixel.system.frontEnds.ConsoleFrontEnd; import flixel.system.frontEnds.DebuggerFrontEnd; import flixel.system.frontEnds.InputFrontEnd; +import flixel.system.frontEnds.InputTextFrontEnd; import flixel.system.frontEnds.LogFrontEnd; import flixel.system.frontEnds.PluginFrontEnd; import flixel.system.frontEnds.SignalFrontEnd; @@ -28,6 +24,11 @@ import flixel.system.scaleModes.RatioScaleMode; import flixel.util.FlxCollision; import flixel.util.FlxSave; import flixel.util.typeLimit.NextState; +import openfl.Lib; +import openfl.display.DisplayObject; +import openfl.display.Stage; +import openfl.display.StageDisplayState; +import openfl.net.URLRequest; #if FLX_TOUCH import flixel.input.touch.FlxTouchManager; #end @@ -320,6 +321,8 @@ class FlxG */ public static var plugins(default, null):PluginFrontEnd; + public static var inputText(default, null):InputTextFrontEnd = new InputTextFrontEnd(); + public static var initialWidth(default, null):Int = 0; public static var initialHeight(default, null):Int = 0; diff --git a/flixel/system/frontEnds/InputTextFrontEnd.hx b/flixel/system/frontEnds/InputTextFrontEnd.hx new file mode 100644 index 0000000000..9d9b35a0f1 --- /dev/null +++ b/flixel/system/frontEnds/InputTextFrontEnd.hx @@ -0,0 +1,204 @@ +package flixel.system.frontEnds; + +import lime.ui.KeyCode; +import lime.ui.KeyModifier; + +class InputTextFrontEnd +{ + public var focus(default, set):Null; + + var _registeredInputTexts:Array = []; + + public function new() {} + + public function registerInputText(input:IFlxInputText) + { + if (!_registeredInputTexts.contains(input)) + { + _registeredInputTexts.push(input); + + if (!FlxG.stage.window.onTextInput.has(onTextInput)) + { + FlxG.stage.window.onTextInput.add(onTextInput); + // Higher priority is needed here because FlxKeyboard will cancel + // the event for key codes in `preventDefaultKeys`. + FlxG.stage.window.onKeyDown.add(onKeyDown, false, 1000); + } + } + } + + public function unregisterInputText(input:IFlxInputText) + { + if (_registeredInputTexts.contains(input)) + { + _registeredInputTexts.remove(input); + + if (_registeredInputTexts.length == 0 && FlxG.stage.window.onTextInput.has(onTextInput)) + { + FlxG.stage.window.onTextInput.remove(onTextInput); + FlxG.stage.window.onKeyDown.remove(onKeyDown); + } + } + } + + function onTextInput(text:String) + { + if (focus != null) + { + focus.dispatchTypingAction(ADD_TEXT(text)); + } + } + + function onKeyDown(key:KeyCode, modifier:KeyModifier) + { + // Taken from OpenFL's `TextField` + var modifierPressed = #if mac modifier.metaKey #elseif js(modifier.metaKey || modifier.ctrlKey) #else (modifier.ctrlKey && !modifier.altKey) #end; + + switch (key) + { + case RETURN, NUMPAD_ENTER: + focus.dispatchTypingAction(COMMAND(NEW_LINE)); + case BACKSPACE: + focus.dispatchTypingAction(COMMAND(DELETE_LEFT)); + case DELETE: + focus.dispatchTypingAction(COMMAND(DELETE_RIGHT)); + case LEFT: + if (modifierPressed) + { + focus.dispatchTypingAction(MOVE_CURSOR(PREVIOUS_LINE, modifier.shiftKey)); + } + else + { + focus.dispatchTypingAction(MOVE_CURSOR(LEFT, modifier.shiftKey)); + } + case RIGHT: + if (modifierPressed) + { + focus.dispatchTypingAction(MOVE_CURSOR(NEXT_LINE, modifier.shiftKey)); + } + else + { + focus.dispatchTypingAction(MOVE_CURSOR(RIGHT, modifier.shiftKey)); + } + case UP: + if (modifierPressed) + { + focus.dispatchTypingAction(MOVE_CURSOR(HOME, modifier.shiftKey)); + } + else + { + focus.dispatchTypingAction(MOVE_CURSOR(UP, modifier.shiftKey)); + } + case DOWN: + if (modifierPressed) + { + focus.dispatchTypingAction(MOVE_CURSOR(END, modifier.shiftKey)); + } + else + { + focus.dispatchTypingAction(MOVE_CURSOR(DOWN, modifier.shiftKey)); + } + case HOME: + if (modifierPressed) + { + focus.dispatchTypingAction(MOVE_CURSOR(HOME, modifier.shiftKey)); + } + else + { + focus.dispatchTypingAction(MOVE_CURSOR(LINE_BEGINNING, modifier.shiftKey)); + } + case END: + if (modifierPressed) + { + focus.dispatchTypingAction(MOVE_CURSOR(END, modifier.shiftKey)); + } + else + { + focus.dispatchTypingAction(MOVE_CURSOR(LINE_END, modifier.shiftKey)); + } + case C: + if (modifierPressed) + { + focus.dispatchTypingAction(COMMAND(COPY)); + } + case X: + if (modifierPressed) + { + focus.dispatchTypingAction(COMMAND(CUT)); + } + #if !js + case V: + if (modifierPressed) + { + focus.dispatchTypingAction(COMMAND(PASTE)); + } + #end + case A: + if (modifierPressed) + { + focus.dispatchTypingAction(COMMAND(SELECT_ALL)); + } + default: + } + } + + function set_focus(value:IFlxInputText) + { + if (focus != value) + { + if (focus != null) + { + focus.hasFocus = false; + } + + focus = value; + + if (focus != null) + { + focus.hasFocus = true; + } + + FlxG.stage.window.textInputEnabled = (focus != null); + } + + return value; + } +} + +interface IFlxInputText +{ + public var hasFocus(default, set):Bool; + public function dispatchTypingAction(action:TypingAction):Void; +} + +enum TypingAction +{ + ADD_TEXT(text:String); + MOVE_CURSOR(type:MoveCursorAction, shiftKey:Bool); + COMMAND(cmd:TypingCommand); +} + +enum MoveCursorAction +{ + LEFT; + RIGHT; + UP; + DOWN; + HOME; + END; + LINE_BEGINNING; + LINE_END; + PREVIOUS_LINE; + NEXT_LINE; +} + +enum TypingCommand +{ + NEW_LINE; + DELETE_LEFT; + DELETE_RIGHT; + COPY; + CUT; + PASTE; + SELECT_ALL; +} \ No newline at end of file diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx new file mode 100644 index 0000000000..d15642a9ae --- /dev/null +++ b/flixel/text/FlxInputText.hx @@ -0,0 +1,579 @@ +package flixel.text; + +import flixel.math.FlxMath; +import flixel.system.frontEnds.InputTextFrontEnd; +import flixel.util.FlxColor; +import lime.system.Clipboard; +import openfl.utils.QName; + +class FlxInputText extends FlxText implements IFlxInputText +{ + static inline var GUTTER:Int = 2; + + public var caretColor(default, set):FlxColor = FlxColor.WHITE; + + public var caretIndex(default, set):Int = -1; + + public var caretWidth(default, set):Int = 1; + + public var hasFocus(default, set):Bool = false; + + public var maxLength(default, set):Int = 0; + + public var passwordMode(get, set):Bool; + + public var selectionBeginIndex(get, never):Int; + + public var selectionEndIndex(get, never):Int; + + var caret:FlxSprite; + + var selectionIndex:Int = -1; + + public function new(x:Float = 0, y:Float = 0, fieldWidth:Float = 0, ?text:String, size:Int = 8, embeddedFont:Bool = true) + { + super(x, y, fieldWidth, text, size, embeddedFont); + + // If the text field's type isn't INPUT and there's a new line at the end + // of the text, it won't be counted for in `numLines` + textField.type = INPUT; + + caret = new FlxSprite(); + caret.moves = caret.active = caret.visible = false; + regenCaret(); + updateCaretPosition(); + + FlxG.inputText.registerInputText(this); + } + + override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (visible) + { + if (FlxG.mouse.justPressed) + { + if (FlxG.mouse.overlaps(this)) + { + hasFocus = true; + } + else + { + hasFocus = false; + } + } + } + } + + override function draw():Void + { + super.draw(); + + drawSprite(caret); + } + + override function destroy():Void + { + FlxG.inputText.unregisterInputText(this); + + super.destroy(); + } + + public function dispatchTypingAction(action:TypingAction):Void + { + switch (action) + { + case ADD_TEXT(text): + replaceSelectedText(text); + case MOVE_CURSOR(type, shiftKey): + moveCursor(type, shiftKey); + case COMMAND(cmd): + runCommand(cmd); + } + } + + function drawSprite(sprite:FlxSprite):Void + { + if (sprite != null && sprite.visible) + { + sprite.scrollFactor.copyFrom(scrollFactor); + sprite._cameras = _cameras; + sprite.draw(); + } + } + + function getCharIndexOnDifferentLine(charIndex:Int, lineIndex:Int):Int + { + if (charIndex < 0 || charIndex > text.length) + return -1; + if (lineIndex < 0 || lineIndex > textField.numLines - 1) + return -1; + + var x = 0.0; + var charBoundaries = textField.getCharBoundaries(charIndex - 1); + if (charBoundaries != null) + { + x = charBoundaries.right; + } + else + { + x = GUTTER; + } + + var y:Float = GUTTER; + for (i in 0...lineIndex) + { + y += textField.getLineMetrics(i).height; + } + y += textField.getLineMetrics(lineIndex).height / 2; + + return getCharAtPosition(x, y); + } + + function getCharAtPosition(x:Float, y:Float):Int + { + var lineY:Float = GUTTER; + for (line in 0...textField.numLines) + { + var lineOffset = textField.getLineOffset(line); + var lineHeight = textField.getLineMetrics(line).height; + if (y >= lineY && y <= lineY + lineHeight) + { + // check for every character in the line + var lineLength = textField.getLineLength(line); + var lineEndIndex = lineOffset + lineLength; + for (char in 0...lineLength) + { + var boundaries = textField.getCharBoundaries(lineOffset + char); + // reached end of line, return this character + if (boundaries == null) + return lineOffset + char; + if (x <= boundaries.right) + { + if (x <= boundaries.x + (boundaries.width / 2)) + { + return lineOffset + char; + } + else + { + return (lineOffset + char < lineEndIndex) ? lineOffset + char + 1 : lineEndIndex; + } + } + } + + // a character wasn't found, return the last character of the line + return lineEndIndex; + } + + lineY += lineHeight; + } + + return -1; + } + + function moveCursor(type:MoveCursorAction, shiftKey:Bool):Void + { + switch (type) + { + case LEFT: + if (caretIndex > 0) + { + caretIndex--; + } + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + case RIGHT: + if (caretIndex < text.length) + { + caretIndex++; + } + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + case UP: + var lineIndex = textField.getLineIndexOfChar(caretIndex); + if (lineIndex > 0) + { + caretIndex = getCharIndexOnDifferentLine(caretIndex, lineIndex - 1); + } + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + case DOWN: + var lineIndex = textField.getLineIndexOfChar(caretIndex); + if (lineIndex < textField.numLines - 1) + { + caretIndex = getCharIndexOnDifferentLine(caretIndex, lineIndex + 1); + } + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + case HOME: + caretIndex = 0; + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + case END: + caretIndex = text.length; + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + case LINE_BEGINNING: + caretIndex = textField.getLineOffset(textField.getLineIndexOfChar(caretIndex)); + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + case LINE_END: + var lineIndex = textField.getLineIndexOfChar(caretIndex); + if (lineIndex < textField.numLines - 1) + { + caretIndex = textField.getLineOffset(lineIndex + 1) - 1; + } + else + { + caretIndex = text.length; + } + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + case PREVIOUS_LINE: + var lineIndex = textField.getLineIndexOfChar(caretIndex); + if (lineIndex > 0) + { + var index = textField.getLineOffset(lineIndex); + if (caretIndex == index) + { + caretIndex = textField.getLineOffset(lineIndex - 1); + } + else + { + caretIndex = index; + } + } + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + case NEXT_LINE: + var lineIndex = textField.getLineIndexOfChar(caretIndex); + if (lineIndex < textField.numLines - 1) + { + caretIndex = textField.getLineOffset(lineIndex + 1); + } + else + { + caretIndex = text.length; + } + + if (!shiftKey) + { + selectionIndex = caretIndex; + } + } + } + + function regenCaret():Void + { + caret.makeGraphic(caretWidth, Std.int(size + 2), caretColor); + } + + function replaceSelectedText(newText:String):Void + { + if (newText == null) + newText = ""; + if (newText == "" && selectionIndex == caretIndex) + return; + + var beginIndex = selectionBeginIndex; + var endIndex = selectionEndIndex; + + if (beginIndex == endIndex && maxLength > 0 && text.length == maxLength) + return; + + if (beginIndex > text.length) + { + beginIndex = text.length; + } + if (endIndex > text.length) + { + endIndex = text.length; + } + if (endIndex < beginIndex) + { + var cache = endIndex; + endIndex = beginIndex; + beginIndex = cache; + } + if (beginIndex < 0) + { + beginIndex = 0; + } + + replaceText(beginIndex, endIndex, newText); + } + + function replaceText(beginIndex:Int, endIndex:Int, newText:String):Void + { + if (endIndex < beginIndex || beginIndex < 0 || endIndex > text.length || newText == null) + return; + + if (maxLength > 0) + { + var removeLength = (endIndex - beginIndex); + var newMaxLength = maxLength - text.length + removeLength; + + if (newMaxLength <= 0) + { + newText = ""; + } + else if (newMaxLength < newText.length) + { + newText = newText.substr(0, newMaxLength); + } + } + + text = text.substring(0, beginIndex) + newText + text.substring(endIndex); + + selectionIndex = caretIndex = beginIndex + newText.length; + } + + function runCommand(cmd:TypingCommand):Void + { + switch (cmd) + { + case NEW_LINE: + if (textField.multiline) + { + replaceSelectedText("\n"); + } + case DELETE_LEFT: + if (selectionIndex == caretIndex && caretIndex > 0) + { + selectionIndex = caretIndex - 1; + } + + if (selectionIndex != caretIndex) + { + replaceSelectedText(""); + selectionIndex = caretIndex; + } + case DELETE_RIGHT: + if (selectionIndex == caretIndex && caretIndex < text.length) + { + selectionIndex = caretIndex + 1; + } + + if (selectionIndex != caretIndex) + { + replaceSelectedText(""); + selectionIndex = caretIndex; + } + case COPY: + if (caretIndex != selectionIndex && !passwordMode) + { + Clipboard.text = text.substring(caretIndex, selectionIndex); + } + case CUT: + if (caretIndex != selectionIndex && !passwordMode) + { + Clipboard.text = text.substring(caretIndex, selectionIndex); + + replaceSelectedText(""); + } + case PASTE: + if (Clipboard.text != null) + { + replaceSelectedText(Clipboard.text); + } + case SELECT_ALL: + selectionIndex = 0; + caretIndex = text.length; + } + } + + function updateCaretPosition():Void + { + if (textField == null) + return; + + if (text.length == 0) + { + caret.setPosition(x + GUTTER, y + GUTTER); + } + else + { + var lineIndex = textField.getLineIndexOfChar(caretIndex); + var boundaries = textField.getCharBoundaries(caretIndex - 1); + if (boundaries != null) + { + caret.setPosition(x + boundaries.right, y + boundaries.y); + } + else // end of line + { + var lineY:Float = GUTTER; + for (i in 0...lineIndex) + { + lineY += textField.getLineMetrics(i).height; + } + caret.setPosition(x + GUTTER, y + lineY); + } + } + } + + override function set_color(value:FlxColor):FlxColor + { + if (color != value) + { + super.set_color(value); + caretColor = value; + } + + return value; + } + + override function set_text(value:String):String + { + if (text != value) + { + super.set_text(value); + + if (hasFocus) + { + if (text.length < selectionIndex) + { + selectionIndex = text.length; + } + if (text.length < caretIndex) + { + caretIndex = text.length; + } + } + else + { + selectionIndex = 0; + caretIndex = 0; + } + } + + return value; + } + + function set_caretColor(value:FlxColor):FlxColor + { + if (caretColor != value) + { + caretColor = value; + regenCaret(); + } + return value; + } + + function set_caretIndex(value:Int):Int + { + if (caretIndex != value) + { + caretIndex = value; + if (caretIndex < 0) + caretIndex = 0; + if (caretIndex > text.length) + caretIndex = text.length; + updateCaretPosition(); + } + return value; + } + + function set_caretWidth(value:Int):Int + { + if (caretWidth != value) + { + caretWidth = value; + regenCaret(); + } + return value; + } + + function set_hasFocus(value:Bool):Bool + { + if (hasFocus != value) + { + hasFocus = value; + if (hasFocus) + { + FlxG.inputText.focus = this; + + if (caretIndex < 0) + { + caretIndex = text.length; + selectionIndex = caretIndex; + } + + caret.visible = true; + } + else if (FlxG.inputText.focus == this) + { + FlxG.inputText.focus = null; + + if (selectionIndex != caretIndex) + { + selectionIndex = caretIndex; + } + + caret.visible = false; + } + } + return value; + } + + function set_maxLength(value:Int):Int + { + if (maxLength != value) + { + maxLength = value; + if (maxLength > 0 && text.length > maxLength) + { + text = text.substr(0, maxLength); + } + } + + return value; + } + + function get_passwordMode():Bool + { + return textField.displayAsPassword; + } + + function set_passwordMode(value:Bool):Bool + { + return textField.displayAsPassword = value; + } + + function get_selectionBeginIndex():Int + { + return FlxMath.minInt(caretIndex, selectionIndex); + } + + function get_selectionEndIndex():Int + { + return FlxMath.maxInt(caretIndex, selectionIndex); + } +} \ No newline at end of file From 99087576094ad7394e846521761f7672b0686eb9 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Tue, 18 Jun 2024 22:48:28 -0400 Subject: [PATCH 02/14] Fix code climate? --- flixel/system/frontEnds/InputTextFrontEnd.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flixel/system/frontEnds/InputTextFrontEnd.hx b/flixel/system/frontEnds/InputTextFrontEnd.hx index 9d9b35a0f1..12ae5327a5 100644 --- a/flixel/system/frontEnds/InputTextFrontEnd.hx +++ b/flixel/system/frontEnds/InputTextFrontEnd.hx @@ -167,8 +167,8 @@ class InputTextFrontEnd interface IFlxInputText { - public var hasFocus(default, set):Bool; - public function dispatchTypingAction(action:TypingAction):Void; + var hasFocus(default, set):Bool; + function dispatchTypingAction(action:TypingAction):Void; } enum TypingAction From 292283892cc4077ce6971ea4d3835b68342d393f Mon Sep 17 00:00:00 2001 From: Starmapo Date: Tue, 18 Jun 2024 23:46:04 -0400 Subject: [PATCH 03/14] Add missing FLX_MOUSE check --- flixel/text/FlxInputText.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index d15642a9ae..f1a955e718 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -50,6 +50,7 @@ class FlxInputText extends FlxText implements IFlxInputText { super.update(elapsed); + #if FLX_MOUSE if (visible) { if (FlxG.mouse.justPressed) @@ -64,6 +65,7 @@ class FlxInputText extends FlxText implements IFlxInputText } } } + #end } override function draw():Void From dd2ff7ede990fdab33b32eab2ddb80544373a239 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Wed, 19 Jun 2024 09:27:30 -0400 Subject: [PATCH 04/14] Add multiline variable - Regenerate text graphic when `passwordMode` changes --- flixel/text/FlxInputText.hx | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index f1a955e718..4fdd7c2131 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -19,6 +19,8 @@ class FlxInputText extends FlxText implements IFlxInputText public var hasFocus(default, set):Bool = false; public var maxLength(default, set):Int = 0; + + public var multiline(get, set):Bool; public var passwordMode(get, set):Bool; @@ -363,7 +365,7 @@ class FlxInputText extends FlxText implements IFlxInputText switch (cmd) { case NEW_LINE: - if (textField.multiline) + if (multiline) { replaceSelectedText("\n"); } @@ -558,6 +560,23 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } + function get_multiline():Bool + { + return textField.multiline; + } + + function set_multiline(value:Bool):Bool + { + if (textField.multiline != value) + { + textField.multiline = value; + // `wordWrap` will still add new lines even if `multiline` is false, + // let's change it accordingly + wordWrap = value; + _regen = true; + } + return value; + } function get_passwordMode():Bool { @@ -566,7 +585,12 @@ class FlxInputText extends FlxText implements IFlxInputText function set_passwordMode(value:Bool):Bool { - return textField.displayAsPassword = value; + if (textField.displayAsPassword != value) + { + textField.displayAsPassword = value; + _regen = true; + } + return value; } function get_selectionBeginIndex():Int From 54a650c117911b8f58486dce2413f7b1f977f639 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Wed, 19 Jun 2024 11:56:33 -0400 Subject: [PATCH 05/14] Place caret at closest character to mouse --- flixel/text/FlxInputText.hx | 57 ++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index 4fdd7c2131..29aaf20d06 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -1,6 +1,8 @@ package flixel.text; +import flixel.input.FlxPointer; import flixel.math.FlxMath; +import flixel.math.FlxPoint; import flixel.system.frontEnds.InputTextFrontEnd; import flixel.util.FlxColor; import lime.system.Clipboard; @@ -19,7 +21,7 @@ class FlxInputText extends FlxText implements IFlxInputText public var hasFocus(default, set):Bool = false; public var maxLength(default, set):Int = 0; - + public var multiline(get, set):Bool; public var passwordMode(get, set):Bool; @@ -55,17 +57,7 @@ class FlxInputText extends FlxText implements IFlxInputText #if FLX_MOUSE if (visible) { - if (FlxG.mouse.justPressed) - { - if (FlxG.mouse.overlaps(this)) - { - hasFocus = true; - } - else - { - hasFocus = false; - } - } + updateInput(); } #end } @@ -443,6 +435,44 @@ class FlxInputText extends FlxText implements IFlxInputText } } + #if FLX_MOUSE + function updateInput() + { + if (FlxG.mouse.justPressed) + { + updatePointerInput(FlxG.mouse); + } + } + + function updatePointerInput(pointer:FlxPointer) + { + var overlap = false; + var pointerPos = FlxPoint.get(); + for (camera in getCameras()) + { + pointer.getWorldPosition(camera, pointerPos); + if (overlapsPoint(pointerPos, true, camera)) + { + hasFocus = true; + + getScreenPosition(_point, camera); + caretIndex = getCharAtPosition(pointerPos.x - _point.x, pointerPos.y - _point.y); + selectionIndex = caretIndex; + + overlap = true; + break; + } + } + + if (!overlap) + { + hasFocus = false; + } + + pointerPos.put(); + } + #end + override function set_color(value:FlxColor):FlxColor { if (color != value) @@ -560,6 +590,7 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } + function get_multiline():Bool { return textField.multiline; @@ -602,4 +633,4 @@ class FlxInputText extends FlxText implements IFlxInputText { return FlxMath.maxInt(caretIndex, selectionIndex); } -} \ No newline at end of file +} From 73220a2a5371acda1e9762374aa6640fa0cc2c87 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Wed, 19 Jun 2024 16:09:26 -0400 Subject: [PATCH 06/14] Selection boxes + selected text color - Add `setSelection()` function - `FlxInputText` variables are now destroyed properly --- flixel/text/FlxInputText.hx | 331 ++++++++++++++++++++++++++---------- 1 file changed, 245 insertions(+), 86 deletions(-) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index 29aaf20d06..1cecb385e8 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -5,7 +5,10 @@ import flixel.math.FlxMath; import flixel.math.FlxPoint; import flixel.system.frontEnds.InputTextFrontEnd; import flixel.util.FlxColor; +import flixel.util.FlxDestroyUtil; import lime.system.Clipboard; +import openfl.geom.Rectangle; +import openfl.text.TextFormat; import openfl.utils.QName; class FlxInputText extends FlxText implements IFlxInputText @@ -14,7 +17,7 @@ class FlxInputText extends FlxText implements IFlxInputText public var caretColor(default, set):FlxColor = FlxColor.WHITE; - public var caretIndex(default, set):Int = -1; + public var caretIndex(get, set):Int; public var caretWidth(default, set):Int = 1; @@ -25,14 +28,20 @@ class FlxInputText extends FlxText implements IFlxInputText public var multiline(get, set):Bool; public var passwordMode(get, set):Bool; + + public var selectedTextColor(default, set):FlxColor = FlxColor.WHITE; public var selectionBeginIndex(get, never):Int; + + public var selectionColor(default, set):FlxColor = FlxColor.BLACK; public var selectionEndIndex(get, never):Int; - var caret:FlxSprite; - - var selectionIndex:Int = -1; + var _caret:FlxSprite; + var _caretIndex:Int = -1; + var _selectionBoxes:Array = []; + var _selectionFormat:TextFormat = new TextFormat(); + var _selectionIndex:Int = -1; public function new(x:Float = 0, y:Float = 0, fieldWidth:Float = 0, ?text:String, size:Int = 8, embeddedFont:Bool = true) { @@ -42,8 +51,10 @@ class FlxInputText extends FlxText implements IFlxInputText // of the text, it won't be counted for in `numLines` textField.type = INPUT; - caret = new FlxSprite(); - caret.moves = caret.active = caret.visible = false; + _selectionFormat.color = selectedTextColor; + + _caret = new FlxSprite(); + _caret.visible = false; regenCaret(); updateCaretPosition(); @@ -64,17 +75,33 @@ class FlxInputText extends FlxText implements IFlxInputText override function draw():Void { + for (box in _selectionBoxes) + drawSprite(box); + super.draw(); - drawSprite(caret); + drawSprite(_caret); } override function destroy():Void { FlxG.inputText.unregisterInputText(this); + + _caret = FlxDestroyUtil.destroy(_caret); + while (_selectionBoxes.length > 0) + FlxDestroyUtil.destroy(_selectionBoxes.pop()); + _selectionBoxes = null; + _selectionFormat = null; super.destroy(); } + + override function applyFormats(formatAdjusted:TextFormat, useBorderColor:Bool = false):Void + { + super.applyFormats(formatAdjusted, useBorderColor); + + textField.setTextFormat(_selectionFormat, selectionBeginIndex, selectionEndIndex); + } public function dispatchTypingAction(action:TypingAction):Void { @@ -88,6 +115,21 @@ class FlxInputText extends FlxText implements IFlxInputText runCommand(cmd); } } + + public function setSelection(beginIndex:Int, endIndex:Int):Void + { + _selectionIndex = beginIndex; + _caretIndex = endIndex; + _regen = true; // regenerate so the selected text format is applied + + if (textField == null) + return; + + _caret.alpha = (_selectionIndex == _caretIndex) ? 1 : 0; + + updateCaretPosition(); + regenSelectionBoxes(); + } function drawSprite(sprite:FlxSprite):Void { @@ -173,130 +215,205 @@ class FlxInputText extends FlxText implements IFlxInputText switch (type) { case LEFT: - if (caretIndex > 0) + if (_caretIndex > 0) { - caretIndex--; + _caretIndex--; } if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); case RIGHT: - if (caretIndex < text.length) + if (_caretIndex < text.length) { - caretIndex++; + _caretIndex++; } if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); case UP: - var lineIndex = textField.getLineIndexOfChar(caretIndex); + var lineIndex = textField.getLineIndexOfChar(_caretIndex); if (lineIndex > 0) { - caretIndex = getCharIndexOnDifferentLine(caretIndex, lineIndex - 1); + _caretIndex = getCharIndexOnDifferentLine(_caretIndex, lineIndex - 1); } if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); case DOWN: - var lineIndex = textField.getLineIndexOfChar(caretIndex); + var lineIndex = textField.getLineIndexOfChar(_caretIndex); if (lineIndex < textField.numLines - 1) { - caretIndex = getCharIndexOnDifferentLine(caretIndex, lineIndex + 1); + _caretIndex = getCharIndexOnDifferentLine(_caretIndex, lineIndex + 1); } if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); case HOME: - caretIndex = 0; + _caretIndex = 0; if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); case END: - caretIndex = text.length; + _caretIndex = text.length; if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); case LINE_BEGINNING: - caretIndex = textField.getLineOffset(textField.getLineIndexOfChar(caretIndex)); + _caretIndex = textField.getLineOffset(textField.getLineIndexOfChar(_caretIndex)); if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); case LINE_END: - var lineIndex = textField.getLineIndexOfChar(caretIndex); + var lineIndex = textField.getLineIndexOfChar(_caretIndex); if (lineIndex < textField.numLines - 1) { - caretIndex = textField.getLineOffset(lineIndex + 1) - 1; + _caretIndex = textField.getLineOffset(lineIndex + 1) - 1; } else { - caretIndex = text.length; + _caretIndex = text.length; } if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); case PREVIOUS_LINE: - var lineIndex = textField.getLineIndexOfChar(caretIndex); + var lineIndex = textField.getLineIndexOfChar(_caretIndex); if (lineIndex > 0) { var index = textField.getLineOffset(lineIndex); - if (caretIndex == index) + if (_caretIndex == index) { - caretIndex = textField.getLineOffset(lineIndex - 1); + _caretIndex = textField.getLineOffset(lineIndex - 1); } else { - caretIndex = index; + _caretIndex = index; } } if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); case NEXT_LINE: - var lineIndex = textField.getLineIndexOfChar(caretIndex); + var lineIndex = textField.getLineIndexOfChar(_caretIndex); if (lineIndex < textField.numLines - 1) { - caretIndex = textField.getLineOffset(lineIndex + 1); + _caretIndex = textField.getLineOffset(lineIndex + 1); } else { - caretIndex = text.length; + _caretIndex = text.length; } if (!shiftKey) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } + setSelection(_selectionIndex, _caretIndex); } } function regenCaret():Void { - caret.makeGraphic(caretWidth, Std.int(size + 2), caretColor); + _caret.makeGraphic(caretWidth, Std.int(size + 2), caretColor); + } + + function regenSelectionBoxes():Void + { + if (textField == null) + return; + + while (_selectionBoxes.length > textField.numLines) + { + var box = _selectionBoxes.pop(); + if (box != null) + box.destroy(); + } + + if (_caretIndex == _selectionIndex) + { + for (box in _selectionBoxes) + { + if (box != null) + box.visible = false; + } + + return; + } + + var beginLine = textField.getLineIndexOfChar(selectionBeginIndex); + var endLine = textField.getLineIndexOfChar(selectionEndIndex); + + for (line in 0...textField.numLines) + { + if (line >= beginLine && line <= endLine) + { + var lineStartIndex = textField.getLineOffset(line); + var lineEndIndex = lineStartIndex + textField.getLineLength(line); + + var startIndex = FlxMath.maxInt(lineStartIndex, selectionBeginIndex); + var endIndex = FlxMath.minInt(lineEndIndex, selectionEndIndex); + + var startBoundaries = textField.getCharBoundaries(startIndex); + var endBoundaries = textField.getCharBoundaries(endIndex - 1); + if (endBoundaries == null && endIndex > startIndex) // end of line, try getting the previous character + { + endBoundaries = textField.getCharBoundaries(endIndex - 2); + } + + if (startBoundaries != null && endBoundaries != null) + { + if (_selectionBoxes[line] == null) + _selectionBoxes[line] = new FlxSprite().makeGraphic(1, 1, selectionColor); + + _selectionBoxes[line].setPosition(x + startBoundaries.x, y + startBoundaries.y); + _selectionBoxes[line].setGraphicSize(endBoundaries.right - startBoundaries.x, startBoundaries.height); + _selectionBoxes[line].updateHitbox(); + _selectionBoxes[line].visible = true; + } + else if (_selectionBoxes[line] != null) + { + _selectionBoxes[line].visible = false; + } + } + else if (_selectionBoxes[line] != null) + { + _selectionBoxes[line].visible = false; + } + } } function replaceSelectedText(newText:String):Void { if (newText == null) newText = ""; - if (newText == "" && selectionIndex == caretIndex) + if (newText == "" && _selectionIndex == _caretIndex) return; var beginIndex = selectionBeginIndex; @@ -349,7 +466,8 @@ class FlxInputText extends FlxText implements IFlxInputText text = text.substring(0, beginIndex) + newText + text.substring(endIndex); - selectionIndex = caretIndex = beginIndex + newText.length; + _selectionIndex = _caretIndex = beginIndex + newText.length; + setSelection(_selectionIndex, _caretIndex); } function runCommand(cmd:TypingCommand):Void @@ -362,36 +480,36 @@ class FlxInputText extends FlxText implements IFlxInputText replaceSelectedText("\n"); } case DELETE_LEFT: - if (selectionIndex == caretIndex && caretIndex > 0) + if (_selectionIndex == _caretIndex && _caretIndex > 0) { - selectionIndex = caretIndex - 1; + _selectionIndex = _caretIndex - 1; } - if (selectionIndex != caretIndex) + if (_selectionIndex != _caretIndex) { replaceSelectedText(""); - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } case DELETE_RIGHT: - if (selectionIndex == caretIndex && caretIndex < text.length) + if (_selectionIndex == _caretIndex && _caretIndex < text.length) { - selectionIndex = caretIndex + 1; + _selectionIndex = _caretIndex + 1; } - if (selectionIndex != caretIndex) + if (_selectionIndex != _caretIndex) { replaceSelectedText(""); - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; } case COPY: - if (caretIndex != selectionIndex && !passwordMode) + if (_caretIndex != _selectionIndex && !passwordMode) { - Clipboard.text = text.substring(caretIndex, selectionIndex); + Clipboard.text = text.substring(_caretIndex, _selectionIndex); } case CUT: - if (caretIndex != selectionIndex && !passwordMode) + if (_caretIndex != _selectionIndex && !passwordMode) { - Clipboard.text = text.substring(caretIndex, selectionIndex); + Clipboard.text = text.substring(_caretIndex, _selectionIndex); replaceSelectedText(""); } @@ -401,8 +519,9 @@ class FlxInputText extends FlxText implements IFlxInputText replaceSelectedText(Clipboard.text); } case SELECT_ALL: - selectionIndex = 0; - caretIndex = text.length; + _selectionIndex = 0; + _caretIndex = text.length; + setSelection(_selectionIndex, _caretIndex); } } @@ -413,30 +532,30 @@ class FlxInputText extends FlxText implements IFlxInputText if (text.length == 0) { - caret.setPosition(x + GUTTER, y + GUTTER); + _caret.setPosition(x + GUTTER, y + GUTTER); } else { - var lineIndex = textField.getLineIndexOfChar(caretIndex); - var boundaries = textField.getCharBoundaries(caretIndex - 1); + var boundaries = textField.getCharBoundaries(_caretIndex - 1); if (boundaries != null) { - caret.setPosition(x + boundaries.right, y + boundaries.y); + _caret.setPosition(x + boundaries.right, y + boundaries.y); } else // end of line { var lineY:Float = GUTTER; + var lineIndex = textField.getLineIndexOfChar(_caretIndex); for (i in 0...lineIndex) { lineY += textField.getLineMetrics(i).height; } - caret.setPosition(x + GUTTER, y + lineY); + _caret.setPosition(x + GUTTER, y + lineY); } } } #if FLX_MOUSE - function updateInput() + function updateInput():Void { if (FlxG.mouse.justPressed) { @@ -444,7 +563,7 @@ class FlxInputText extends FlxText implements IFlxInputText } } - function updatePointerInput(pointer:FlxPointer) + function updatePointerInput(pointer:FlxPointer):Void { var overlap = false; var pointerPos = FlxPoint.get(); @@ -456,8 +575,9 @@ class FlxInputText extends FlxText implements IFlxInputText hasFocus = true; getScreenPosition(_point, camera); - caretIndex = getCharAtPosition(pointerPos.x - _point.x, pointerPos.y - _point.y); - selectionIndex = caretIndex; + _caretIndex = getCharAtPosition(pointerPos.x - _point.x, pointerPos.y - _point.y); + _selectionIndex = _caretIndex; + setSelection(_selectionIndex, _caretIndex); overlap = true; break; @@ -492,20 +612,21 @@ class FlxInputText extends FlxText implements IFlxInputText if (hasFocus) { - if (text.length < selectionIndex) + if (text.length < _selectionIndex) { - selectionIndex = text.length; + _selectionIndex = text.length; } - if (text.length < caretIndex) + if (text.length < _caretIndex) { - caretIndex = text.length; + _caretIndex = text.length; } } else { - selectionIndex = 0; - caretIndex = 0; + _selectionIndex = 0; + _caretIndex = 0; } + setSelection(_selectionIndex, _caretIndex); } return value; @@ -518,20 +639,26 @@ class FlxInputText extends FlxText implements IFlxInputText caretColor = value; regenCaret(); } + return value; } + function get_caretIndex():Int + { + return _caretIndex; + } function set_caretIndex(value:Int):Int { - if (caretIndex != value) + if (_caretIndex != value) { - caretIndex = value; - if (caretIndex < 0) - caretIndex = 0; - if (caretIndex > text.length) - caretIndex = text.length; - updateCaretPosition(); + _caretIndex = value; + if (_caretIndex < 0) + _caretIndex = 0; + if (_caretIndex > text.length) + _caretIndex = text.length; + setSelection(_caretIndex, _caretIndex); } + return value; } @@ -542,6 +669,7 @@ class FlxInputText extends FlxText implements IFlxInputText caretWidth = value; regenCaret(); } + return value; } @@ -554,26 +682,29 @@ class FlxInputText extends FlxText implements IFlxInputText { FlxG.inputText.focus = this; - if (caretIndex < 0) + if (_caretIndex < 0) { - caretIndex = text.length; - selectionIndex = caretIndex; + _caretIndex = text.length; + _selectionIndex = _caretIndex; + setSelection(_selectionIndex, _caretIndex); } - caret.visible = true; + _caret.visible = true; } else if (FlxG.inputText.focus == this) { FlxG.inputText.focus = null; - if (selectionIndex != caretIndex) + if (_selectionIndex != _caretIndex) { - selectionIndex = caretIndex; + _selectionIndex = _caretIndex; + setSelection(_selectionIndex, _caretIndex); } - caret.visible = false; + _caret.visible = false; } } + return value; } @@ -606,6 +737,7 @@ class FlxInputText extends FlxText implements IFlxInputText wordWrap = value; _regen = true; } + return value; } @@ -624,13 +756,40 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } + function set_selectedTextColor(value:FlxColor):FlxColor + { + if (selectedTextColor != value) + { + selectedTextColor = value; + _selectionFormat.color = selectedTextColor; + _regen = true; + } + + return value; + } + function get_selectionBeginIndex():Int { - return FlxMath.minInt(caretIndex, selectionIndex); + return FlxMath.minInt(_caretIndex, _selectionIndex); + } + + function set_selectionColor(value:FlxColor):FlxColor + { + if (selectionColor != value) + { + selectionColor = value; + for (box in _selectionBoxes) + { + if (box != null) + box.makeGraphic(1, 1, selectionColor); + } + } + + return value; } function get_selectionEndIndex():Int { - return FlxMath.maxInt(caretIndex, selectionIndex); + return FlxMath.maxInt(_caretIndex, _selectionIndex); } } From 99055c79ebbb6c0d1c8296df58fbb681e1ce17f3 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Wed, 19 Jun 2024 20:23:20 -0400 Subject: [PATCH 07/14] Implement text selection with mouse & text scrolling - Added `scrollH`, `scrollV`, `bottomScrollV`, `maxScrollH` & `maxScrollV` variables - Return end of text if character isn't found at position --- flixel/text/FlxInputText.hx | 315 ++++++++++++++++++++++++++---------- 1 file changed, 234 insertions(+), 81 deletions(-) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index 1cecb385e8..5b14d6d66e 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -7,6 +7,7 @@ import flixel.system.frontEnds.InputTextFrontEnd; import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; import lime.system.Clipboard; +import openfl.display.BitmapData; import openfl.geom.Rectangle; import openfl.text.TextFormat; import openfl.utils.QName; @@ -15,6 +16,8 @@ class FlxInputText extends FlxText implements IFlxInputText { static inline var GUTTER:Int = 2; + public var bottomScrollV(get, never):Int; + public var caretColor(default, set):FlxColor = FlxColor.WHITE; public var caretIndex(get, set):Int; @@ -24,11 +27,19 @@ class FlxInputText extends FlxText implements IFlxInputText public var hasFocus(default, set):Bool = false; public var maxLength(default, set):Int = 0; + + public var maxScrollH(get, never):Int; + + public var maxScrollV(get, never):Int; public var multiline(get, set):Bool; public var passwordMode(get, set):Bool; + public var scrollH(get, set):Int; + + public var scrollV(get, set):Int; + public var selectedTextColor(default, set):FlxColor = FlxColor.WHITE; public var selectionBeginIndex(get, never):Int; @@ -39,6 +50,8 @@ class FlxInputText extends FlxText implements IFlxInputText var _caret:FlxSprite; var _caretIndex:Int = -1; + var _mouseDown:Bool; + var _pointerCamera:FlxCamera; var _selectionBoxes:Array = []; var _selectionFormat:TextFormat = new TextFormat(); var _selectionIndex:Int = -1; @@ -88,6 +101,7 @@ class FlxInputText extends FlxText implements IFlxInputText FlxG.inputText.unregisterInputText(this); _caret = FlxDestroyUtil.destroy(_caret); + _pointerCamera = null; while (_selectionBoxes.length > 0) FlxDestroyUtil.destroy(_selectionBoxes.pop()); _selectionBoxes = null; @@ -120,15 +134,11 @@ class FlxInputText extends FlxText implements IFlxInputText { _selectionIndex = beginIndex; _caretIndex = endIndex; - _regen = true; // regenerate so the selected text format is applied if (textField == null) return; - - _caret.alpha = (_selectionIndex == _caretIndex) ? 1 : 0; - - updateCaretPosition(); - regenSelectionBoxes(); + + updateSelection(); } function drawSprite(sprite:FlxSprite):Void @@ -160,9 +170,13 @@ class FlxInputText extends FlxText implements IFlxInputText } var y:Float = GUTTER; - for (i in 0...lineIndex) + for (i in 0...FlxMath.maxInt(scrollV - 1, lineIndex)) { - y += textField.getLineMetrics(i).height; + var lineHeight = textField.getLineMetrics(i).height; + if (i < scrollV - 1) + y -= lineHeight; + if (i < lineIndex) + y += lineHeight; } y += textField.getLineMetrics(lineIndex).height / 2; @@ -171,6 +185,14 @@ class FlxInputText extends FlxText implements IFlxInputText function getCharAtPosition(x:Float, y:Float):Int { + x += scrollH; + for (i in 0...scrollV - 1) + { + y += textField.getLineMetrics(i).height; + } + if (y > textField.textHeight) + y = textField.textHeight; + var lineY:Float = GUTTER; for (line in 0...textField.numLines) { @@ -207,7 +229,7 @@ class FlxInputText extends FlxText implements IFlxInputText lineY += lineHeight; } - return -1; + return text.length; } function moveCursor(type:MoveCursorAction, shiftKey:Bool):Void @@ -343,72 +365,7 @@ class FlxInputText extends FlxText implements IFlxInputText { _caret.makeGraphic(caretWidth, Std.int(size + 2), caretColor); } - - function regenSelectionBoxes():Void - { - if (textField == null) - return; - - while (_selectionBoxes.length > textField.numLines) - { - var box = _selectionBoxes.pop(); - if (box != null) - box.destroy(); - } - - if (_caretIndex == _selectionIndex) - { - for (box in _selectionBoxes) - { - if (box != null) - box.visible = false; - } - - return; - } - - var beginLine = textField.getLineIndexOfChar(selectionBeginIndex); - var endLine = textField.getLineIndexOfChar(selectionEndIndex); - - for (line in 0...textField.numLines) - { - if (line >= beginLine && line <= endLine) - { - var lineStartIndex = textField.getLineOffset(line); - var lineEndIndex = lineStartIndex + textField.getLineLength(line); - - var startIndex = FlxMath.maxInt(lineStartIndex, selectionBeginIndex); - var endIndex = FlxMath.minInt(lineEndIndex, selectionEndIndex); - - var startBoundaries = textField.getCharBoundaries(startIndex); - var endBoundaries = textField.getCharBoundaries(endIndex - 1); - if (endBoundaries == null && endIndex > startIndex) // end of line, try getting the previous character - { - endBoundaries = textField.getCharBoundaries(endIndex - 2); - } - - if (startBoundaries != null && endBoundaries != null) - { - if (_selectionBoxes[line] == null) - _selectionBoxes[line] = new FlxSprite().makeGraphic(1, 1, selectionColor); - - _selectionBoxes[line].setPosition(x + startBoundaries.x, y + startBoundaries.y); - _selectionBoxes[line].setGraphicSize(endBoundaries.right - startBoundaries.x, startBoundaries.height); - _selectionBoxes[line].updateHitbox(); - _selectionBoxes[line].visible = true; - } - else if (_selectionBoxes[line] != null) - { - _selectionBoxes[line].visible = false; - } - } - else if (_selectionBoxes[line] != null) - { - _selectionBoxes[line].visible = false; - } - } - } - + function replaceSelectedText(newText:String):Void { if (newText == null) @@ -536,10 +493,15 @@ class FlxInputText extends FlxText implements IFlxInputText } else { + var scrollY = 0.0; + for (i in 0...scrollV - 1) + { + scrollY += textField.getLineMetrics(i).height; + } var boundaries = textField.getCharBoundaries(_caretIndex - 1); if (boundaries != null) { - _caret.setPosition(x + boundaries.right, y + boundaries.y); + _caret.setPosition(x + boundaries.right - scrollH, y + boundaries.y - scrollY); } else // end of line { @@ -549,7 +511,88 @@ class FlxInputText extends FlxText implements IFlxInputText { lineY += textField.getLineMetrics(i).height; } - _caret.setPosition(x + GUTTER, y + lineY); + _caret.setPosition(x + GUTTER, y + lineY - scrollY); + } + } + } + + function updateSelection():Void + { + textField.setSelection(_selectionIndex, _caretIndex); + _caret.alpha = (_selectionIndex == _caretIndex) ? 1 : 0; + updateCaretPosition(); + updateSelectionBoxes(); + + _regen = true; + } + + function updateSelectionBoxes():Void + { + if (textField == null) + return; + + while (_selectionBoxes.length > textField.numLines) + { + var box = _selectionBoxes.pop(); + if (box != null) + box.destroy(); + } + + if (_caretIndex == _selectionIndex) + { + for (box in _selectionBoxes) + { + if (box != null) + box.visible = false; + } + + return; + } + + var beginLine = textField.getLineIndexOfChar(selectionBeginIndex); + var endLine = textField.getLineIndexOfChar(selectionEndIndex); + + var scrollY = 0.0; + for (i in 0...scrollV - 1) + { + scrollY += textField.getLineMetrics(i).height; + } + + for (line in 0...textField.numLines) + { + if ((line >= scrollV - 1 && line <= bottomScrollV - 1) && (line >= beginLine && line <= endLine)) + { + var lineStartIndex = textField.getLineOffset(line); + var lineEndIndex = lineStartIndex + textField.getLineLength(line); + + var startIndex = FlxMath.maxInt(lineStartIndex, selectionBeginIndex); + var endIndex = FlxMath.minInt(lineEndIndex, selectionEndIndex); + + var startBoundaries = textField.getCharBoundaries(startIndex); + var endBoundaries = textField.getCharBoundaries(endIndex - 1); + if (endBoundaries == null && endIndex > startIndex) // end of line, try getting the previous character + { + endBoundaries = textField.getCharBoundaries(endIndex - 2); + } + + if (startBoundaries != null && endBoundaries != null) + { + if (_selectionBoxes[line] == null) + _selectionBoxes[line] = new FlxSprite().makeGraphic(1, 1, selectionColor); + + _selectionBoxes[line].setPosition(x + startBoundaries.x - scrollH, y + startBoundaries.y - scrollY); + _selectionBoxes[line].setGraphicSize(endBoundaries.right - startBoundaries.x, startBoundaries.height); + _selectionBoxes[line].updateHitbox(); + _selectionBoxes[line].visible = true; + } + else if (_selectionBoxes[line] != null) + { + _selectionBoxes[line].visible = false; + } + } + else if (_selectionBoxes[line] != null) + { + _selectionBoxes[line].visible = false; } } } @@ -557,13 +600,26 @@ class FlxInputText extends FlxText implements IFlxInputText #if FLX_MOUSE function updateInput():Void { + if (_mouseDown) + { + if (FlxG.mouse.justMoved) + { + updatePointerDrag(FlxG.mouse); + } + + if (FlxG.mouse.released) + { + _mouseDown = false; + updatePointerRelease(FlxG.mouse); + } + } if (FlxG.mouse.justPressed) { - updatePointerInput(FlxG.mouse); + _mouseDown = checkPointerOverlap(FlxG.mouse); } } - function updatePointerInput(pointer:FlxPointer):Void + function checkPointerOverlap(pointer:FlxPointer):Bool { var overlap = false; var pointerPos = FlxPoint.get(); @@ -573,13 +629,15 @@ class FlxInputText extends FlxText implements IFlxInputText if (overlapsPoint(pointerPos, true, camera)) { hasFocus = true; + _pointerCamera = camera; - getScreenPosition(_point, camera); - _caretIndex = getCharAtPosition(pointerPos.x - _point.x, pointerPos.y - _point.y); + var relativePos = getRelativePosition(pointerPos); + _caretIndex = getCharAtPosition(relativePos.x, relativePos.y); _selectionIndex = _caretIndex; setSelection(_selectionIndex, _caretIndex); overlap = true; + relativePos.put(); break; } } @@ -590,6 +648,51 @@ class FlxInputText extends FlxText implements IFlxInputText } pointerPos.put(); + return overlap; + } + + function updatePointerDrag(pointer:FlxPointer):Void + { + if (_selectionIndex < 0) + return; + + var pointerPos = pointer.getWorldPosition(_pointerCamera); + var relativePos = getRelativePosition(pointerPos); + + var char = getCharAtPosition(relativePos.x, relativePos.y); + if (char != _caretIndex) + { + _caretIndex = char; + updateSelection(); + } + + pointerPos.put(); + relativePos.put(); + } + + function updatePointerRelease(pointer:FlxPointer):Void + { + if (!hasFocus) + return; + + var pointerPos = pointer.getWorldPosition(_pointerCamera); + var relativePos = getRelativePosition(pointerPos); + + var upPos = getCharAtPosition(relativePos.x, relativePos.y); + var leftPos = FlxMath.minInt(_selectionIndex, upPos); + var rightPos = FlxMath.maxInt(_selectionIndex, upPos); + + _selectionIndex = leftPos; + _caretIndex = rightPos; + + pointerPos.put(); + relativePos.put(); + } + + function getRelativePosition(point:FlxPoint) + { + getScreenPosition(_point, _pointerCamera); + return FlxPoint.get(point.x - _point.x, point.y - _point.y); } #end @@ -631,6 +734,10 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } + function get_bottomScrollV():Int + { + return textField.bottomScrollV; + } function set_caretColor(value:FlxColor):FlxColor { @@ -721,6 +828,15 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } + function get_maxScrollH():Int + { + return textField.maxScrollH; + } + + function get_maxScrollV():Int + { + return textField.maxScrollV; + } function get_multiline():Bool { @@ -755,6 +871,43 @@ class FlxInputText extends FlxText implements IFlxInputText } return value; } + function get_scrollH():Int + { + return textField.scrollH; + } + + function set_scrollH(value:Int):Int + { + if (value > maxScrollH) + value = maxScrollH; + if (value < 0) + value = 0; + if (textField.scrollH != value) + { + textField.scrollH = value; + updateSelection(); + } + return value; + } + + function get_scrollV():Int + { + return textField.scrollV; + } + + function set_scrollV(value:Int):Int + { + if (value > maxScrollV) + value = maxScrollV; + if (value < 1) + value = 1; + if (textField.scrollV != value || textField.scrollV == 0) + { + textField.scrollV = value; + updateSelection(); + } + return value; + } function set_selectedTextColor(value:FlxColor):FlxColor { From 8ea7c586964cc9618d207b38d4fa1484f9f1f313 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Thu, 20 Jun 2024 20:46:43 -0400 Subject: [PATCH 08/14] Fix selection not working correctly when mouse is out of bounds - Selection boxes are now clipped inside the text bounds - Simplified getting the Y offset of a line --- flixel/text/FlxInputText.hx | 83 +++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index 5b14d6d66e..ae9daef3ec 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -3,6 +3,7 @@ package flixel.text; import flixel.input.FlxPointer; import flixel.math.FlxMath; import flixel.math.FlxPoint; +import flixel.math.FlxRect; import flixel.system.frontEnds.InputTextFrontEnd; import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; @@ -169,16 +170,7 @@ class FlxInputText extends FlxText implements IFlxInputText x = GUTTER; } - var y:Float = GUTTER; - for (i in 0...FlxMath.maxInt(scrollV - 1, lineIndex)) - { - var lineHeight = textField.getLineMetrics(i).height; - if (i < scrollV - 1) - y -= lineHeight; - if (i < lineIndex) - y += lineHeight; - } - y += textField.getLineMetrics(lineIndex).height / 2; + var y = GUTTER + getLineY(lineIndex) + textField.getLineMetrics(lineIndex).height / 2 - getLineY(scrollV - 1); return getCharAtPosition(x, y); } @@ -186,16 +178,19 @@ class FlxInputText extends FlxText implements IFlxInputText function getCharAtPosition(x:Float, y:Float):Int { x += scrollH; - for (i in 0...scrollV - 1) - { - y += textField.getLineMetrics(i).height; - } + y += getLineY(scrollV - 1); + + if (x < GUTTER) + x = GUTTER; + if (y > textField.textHeight) y = textField.textHeight; + if (y < GUTTER) + y = GUTTER; - var lineY:Float = GUTTER; for (line in 0...textField.numLines) { + var lineY = GUTTER + getLineY(line); var lineOffset = textField.getLineOffset(line); var lineHeight = textField.getLineMetrics(line).height; if (y >= lineY && y <= lineY + lineHeight) @@ -225,12 +220,19 @@ class FlxInputText extends FlxText implements IFlxInputText // a character wasn't found, return the last character of the line return lineEndIndex; } - - lineY += lineHeight; } return text.length; } + function getLineY(line:Int):Float + { + var scrollY = 0.0; + for (i in 0...line) + { + scrollY += textField.getLineMetrics(i).height; + } + return scrollY; + } function moveCursor(type:MoveCursorAction, shiftKey:Bool):Void { @@ -493,25 +495,15 @@ class FlxInputText extends FlxText implements IFlxInputText } else { - var scrollY = 0.0; - for (i in 0...scrollV - 1) - { - scrollY += textField.getLineMetrics(i).height; - } var boundaries = textField.getCharBoundaries(_caretIndex - 1); if (boundaries != null) { - _caret.setPosition(x + boundaries.right - scrollH, y + boundaries.y - scrollY); + _caret.setPosition(x + boundaries.right - scrollH, y + boundaries.y - getLineY(scrollV - 1)); } else // end of line { - var lineY:Float = GUTTER; var lineIndex = textField.getLineIndexOfChar(_caretIndex); - for (i in 0...lineIndex) - { - lineY += textField.getLineMetrics(i).height; - } - _caret.setPosition(x + GUTTER, y + lineY - scrollY); + _caret.setPosition(x + GUTTER, y + GUTTER + getLineY(lineIndex) - getLineY(scrollV - 1)); } } } @@ -552,14 +544,11 @@ class FlxInputText extends FlxText implements IFlxInputText var beginLine = textField.getLineIndexOfChar(selectionBeginIndex); var endLine = textField.getLineIndexOfChar(selectionEndIndex); - var scrollY = 0.0; - for (i in 0...scrollV - 1) - { - scrollY += textField.getLineMetrics(i).height; - } + var scrollY = getLineY(scrollV - 1); for (line in 0...textField.numLines) { + var box = _selectionBoxes[line]; if ((line >= scrollV - 1 && line <= bottomScrollV - 1) && (line >= beginLine && line <= endLine)) { var lineStartIndex = textField.getLineOffset(line); @@ -577,22 +566,28 @@ class FlxInputText extends FlxText implements IFlxInputText if (startBoundaries != null && endBoundaries != null) { - if (_selectionBoxes[line] == null) - _selectionBoxes[line] = new FlxSprite().makeGraphic(1, 1, selectionColor); + if (box == null) + box = _selectionBoxes[line] = new FlxSprite().makeGraphic(1, 1, selectionColor); - _selectionBoxes[line].setPosition(x + startBoundaries.x - scrollH, y + startBoundaries.y - scrollY); - _selectionBoxes[line].setGraphicSize(endBoundaries.right - startBoundaries.x, startBoundaries.height); - _selectionBoxes[line].updateHitbox(); - _selectionBoxes[line].visible = true; + var boxRect = FlxRect.get(startBoundaries.x - scrollH, startBoundaries.y - scrollY, endBoundaries.right - startBoundaries.x, + startBoundaries.height); + boxRect.clipTo(FlxRect.weak(0, 0, width, height)); // clip the selection box inside the text sprite + + box.setPosition(x + boxRect.x, y + boxRect.y); + box.setGraphicSize(boxRect.width, boxRect.height); + box.updateHitbox(); + box.visible = true; + + boxRect.put(); } - else if (_selectionBoxes[line] != null) + else if (box != null) { - _selectionBoxes[line].visible = false; + box.visible = false; } } - else if (_selectionBoxes[line] != null) + else if (box != null) { - _selectionBoxes[line].visible = false; + box.visible = false; } } } From 090642b6bc0b3fcb7a395a7f2cd9ebc497bec679 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Thu, 20 Jun 2024 22:58:03 -0400 Subject: [PATCH 09/14] Mouse wheel scrolling - Fix scrollV not being able to be modified directly --- flixel/text/FlxInputText.hx | 97 +++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index ae9daef3ec..39a905d06a 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -113,9 +113,12 @@ class FlxInputText extends FlxText implements IFlxInputText override function applyFormats(formatAdjusted:TextFormat, useBorderColor:Bool = false):Void { + var cache = scrollV; + super.applyFormats(formatAdjusted, useBorderColor); textField.setTextFormat(_selectionFormat, selectionBeginIndex, selectionEndIndex); + scrollV = cache; } public function dispatchTypingAction(action:TypingAction):Void @@ -224,6 +227,7 @@ class FlxInputText extends FlxText implements IFlxInputText return text.length; } + function getLineY(line:Int):Float { var scrollY = 0.0; @@ -233,6 +237,12 @@ class FlxInputText extends FlxText implements IFlxInputText } return scrollY; } + + function isCaretLineVisible():Bool + { + var line = textField.getLineIndexOfChar(_caretIndex); + return line >= scrollV - 1 && line <= bottomScrollV - 1; + } function moveCursor(type:MoveCursorAction, shiftKey:Bool):Void { @@ -511,10 +521,7 @@ class FlxInputText extends FlxText implements IFlxInputText function updateSelection():Void { textField.setSelection(_selectionIndex, _caretIndex); - _caret.alpha = (_selectionIndex == _caretIndex) ? 1 : 0; - updateCaretPosition(); - updateSelectionBoxes(); - + updateSelectionSprites(); _regen = true; } @@ -592,6 +599,13 @@ class FlxInputText extends FlxText implements IFlxInputText } } + function updateSelectionSprites():Void + { + _caret.alpha = (_selectionIndex == _caretIndex && isCaretLineVisible()) ? 1 : 0; + updateCaretPosition(); + updateSelectionBoxes(); + } + #if FLX_MOUSE function updateInput():Void { @@ -608,9 +622,22 @@ class FlxInputText extends FlxText implements IFlxInputText updatePointerRelease(FlxG.mouse); } } - if (FlxG.mouse.justPressed) + if (checkPointerOverlap(FlxG.mouse)) { - _mouseDown = checkPointerOverlap(FlxG.mouse); + if (FlxG.mouse.justPressed) + { + _mouseDown = true; + updatePointerPress(FlxG.mouse); + } + + if (FlxG.mouse.wheel != 0) + { + scrollV = FlxMath.minInt(scrollV - FlxG.mouse.wheel, maxScrollV); + } + } + else if (FlxG.mouse.justPressed) + { + hasFocus = false; } } @@ -623,36 +650,35 @@ class FlxInputText extends FlxText implements IFlxInputText pointer.getWorldPosition(camera, pointerPos); if (overlapsPoint(pointerPos, true, camera)) { - hasFocus = true; - _pointerCamera = camera; - - var relativePos = getRelativePosition(pointerPos); - _caretIndex = getCharAtPosition(relativePos.x, relativePos.y); - _selectionIndex = _caretIndex; - setSelection(_selectionIndex, _caretIndex); - + if (_pointerCamera == null) + _pointerCamera = camera; overlap = true; - relativePos.put(); break; } } - - if (!overlap) - { - hasFocus = false; - } pointerPos.put(); return overlap; } + function updatePointerPress(pointer:FlxPointer):Void + { + hasFocus = true; + + var relativePos = getRelativePosition(pointer); + _caretIndex = getCharAtPosition(relativePos.x, relativePos.y); + _selectionIndex = _caretIndex; + setSelection(_selectionIndex, _caretIndex); + + relativePos.put(); + } + function updatePointerDrag(pointer:FlxPointer):Void { if (_selectionIndex < 0) return; - var pointerPos = pointer.getWorldPosition(_pointerCamera); - var relativePos = getRelativePosition(pointerPos); + var relativePos = getRelativePosition(pointer); var char = getCharAtPosition(relativePos.x, relativePos.y); if (char != _caretIndex) @@ -660,8 +686,7 @@ class FlxInputText extends FlxText implements IFlxInputText _caretIndex = char; updateSelection(); } - - pointerPos.put(); + relativePos.put(); } @@ -670,8 +695,7 @@ class FlxInputText extends FlxText implements IFlxInputText if (!hasFocus) return; - var pointerPos = pointer.getWorldPosition(_pointerCamera); - var relativePos = getRelativePosition(pointerPos); + var relativePos = getRelativePosition(pointer); var upPos = getCharAtPosition(relativePos.x, relativePos.y); var leftPos = FlxMath.minInt(_selectionIndex, upPos); @@ -679,15 +703,18 @@ class FlxInputText extends FlxText implements IFlxInputText _selectionIndex = leftPos; _caretIndex = rightPos; - - pointerPos.put(); + relativePos.put(); + _pointerCamera = null; } - function getRelativePosition(point:FlxPoint) + function getRelativePosition(pointer:FlxPointer):FlxPoint { + var pointerPos = pointer.getWorldPosition(_pointerCamera); getScreenPosition(_point, _pointerCamera); - return FlxPoint.get(point.x - _point.x, point.y - _point.y); + var result = FlxPoint.get(pointerPos.x - _point.x, pointerPos.y - _point.y); + pointerPos.put(); + return result; } #end @@ -729,6 +756,7 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } + function get_bottomScrollV():Int { return textField.bottomScrollV; @@ -749,6 +777,7 @@ class FlxInputText extends FlxText implements IFlxInputText { return _caretIndex; } + function set_caretIndex(value:Int):Int { if (_caretIndex != value) @@ -823,6 +852,7 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } + function get_maxScrollH():Int { return textField.maxScrollH; @@ -866,6 +896,7 @@ class FlxInputText extends FlxText implements IFlxInputText } return value; } + function get_scrollH():Int { return textField.scrollH; @@ -880,7 +911,8 @@ class FlxInputText extends FlxText implements IFlxInputText if (textField.scrollH != value) { textField.scrollH = value; - updateSelection(); + _regen = true; + updateSelectionSprites(); } return value; } @@ -899,7 +931,8 @@ class FlxInputText extends FlxText implements IFlxInputText if (textField.scrollV != value || textField.scrollV == 0) { textField.scrollV = value; - updateSelection(); + _regen = true; + updateSelectionSprites(); } return value; } From 20afded4d51ddb23c9428dcab58232f056c82b15 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Fri, 21 Jun 2024 17:18:31 -0400 Subject: [PATCH 10/14] Implemented double press and dragging - Selection sprites now just change their color instead of making new graphics - scrollH can now be modified properly as well - Word wrap no longer changes with multiline (multiline only affects adding new lines) --- flixel/text/FlxInputText.hx | 120 ++++++++++++++++++++++++++++++------ 1 file changed, 101 insertions(+), 19 deletions(-) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index 39a905d06a..4cf87d5024 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -17,6 +17,8 @@ class FlxInputText extends FlxText implements IFlxInputText { static inline var GUTTER:Int = 2; + static final DELIMITERS:Array = ['\n', '.', '!', '?', ',', ' ', ';', ':', '(', ')', '-', '_', '/']; + public var bottomScrollV(get, never):Int; public var caretColor(default, set):FlxColor = FlxColor.WHITE; @@ -51,8 +53,10 @@ class FlxInputText extends FlxText implements IFlxInputText var _caret:FlxSprite; var _caretIndex:Int = -1; - var _mouseDown:Bool; + var _lastClickTime:Int = 0; + var _mouseDown:Bool = false; var _pointerCamera:FlxCamera; + var _scrollVCounter:Float = 0; var _selectionBoxes:Array = []; var _selectionFormat:TextFormat = new TextFormat(); var _selectionIndex:Int = -1; @@ -82,7 +86,7 @@ class FlxInputText extends FlxText implements IFlxInputText #if FLX_MOUSE if (visible) { - updateInput(); + updateInput(elapsed); } #end } @@ -113,12 +117,27 @@ class FlxInputText extends FlxText implements IFlxInputText override function applyFormats(formatAdjusted:TextFormat, useBorderColor:Bool = false):Void { - var cache = scrollV; - + // scroll variables will be reset when `textField.setTextFormat()` is called, + // cache the current ones first + var cacheScrollH = scrollH; + var cacheScrollV = scrollV; + super.applyFormats(formatAdjusted, useBorderColor); textField.setTextFormat(_selectionFormat, selectionBeginIndex, selectionEndIndex); - scrollV = cache; + // set the scroll back to how it was + scrollH = cacheScrollH; + scrollV = cacheScrollV; + } + + override function regenGraphic():Void + { + var regenSelection = _regen; + + super.regenGraphic(); + + if (_caret != null && regenSelection) + updateSelectionSprites(); } public function dispatchTypingAction(action:TypingAction):Void @@ -375,7 +394,7 @@ class FlxInputText extends FlxText implements IFlxInputText function regenCaret():Void { - _caret.makeGraphic(caretWidth, Std.int(size + 2), caretColor); + _caret.makeGraphic(caretWidth, Std.int(size + 2), FlxColor.WHITE); } function replaceSelectedText(newText:String):Void @@ -521,7 +540,6 @@ class FlxInputText extends FlxText implements IFlxInputText function updateSelection():Void { textField.setSelection(_selectionIndex, _caretIndex); - updateSelectionSprites(); _regen = true; } @@ -574,7 +592,10 @@ class FlxInputText extends FlxText implements IFlxInputText if (startBoundaries != null && endBoundaries != null) { if (box == null) - box = _selectionBoxes[line] = new FlxSprite().makeGraphic(1, 1, selectionColor); + { + box = _selectionBoxes[line] = new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE); + box.color = selectionColor; + } var boxRect = FlxRect.get(startBoundaries.x - scrollH, startBoundaries.y - scrollY, endBoundaries.right - startBoundaries.x, startBoundaries.height); @@ -607,13 +628,15 @@ class FlxInputText extends FlxText implements IFlxInputText } #if FLX_MOUSE - function updateInput():Void + function updateInput(elapsed:Float):Void { if (_mouseDown) { + updatePointerDrag(FlxG.mouse, elapsed); + if (FlxG.mouse.justMoved) { - updatePointerDrag(FlxG.mouse); + updatePointerMove(FlxG.mouse); } if (FlxG.mouse.released) @@ -628,6 +651,16 @@ class FlxInputText extends FlxText implements IFlxInputText { _mouseDown = true; updatePointerPress(FlxG.mouse); + var currentTime = FlxG.game.ticks; + if (currentTime - _lastClickTime < 500) + { + updatePointerDoublePress(FlxG.mouse); + _lastClickTime = 0; + } + else + { + _lastClickTime = currentTime; + } } if (FlxG.mouse.wheel != 0) @@ -672,8 +705,36 @@ class FlxInputText extends FlxText implements IFlxInputText relativePos.put(); } + function updatePointerDrag(pointer:FlxPointer, elapsed:Float) + { + var relativePos = getRelativePosition(pointer); + + if (relativePos.x > width - 1) + { + scrollH += Std.int(Math.max(Math.min((relativePos.x - width) * .1, 10), 1)); + } + else if (relativePos.x < 1) + { + scrollH -= Std.int(Math.max(Math.min(relativePos.x * -.1, 10), 1)); + } + + _scrollVCounter += elapsed; + + if (_scrollVCounter > 0.1) + { + if (relativePos.y > height - 2) + { + scrollV = Std.int(Math.min(scrollV + Math.max(Math.min((relativePos.y - height) * .03, 5), 1), maxScrollV)); + } + else if (relativePos.y < 2) + { + scrollV -= Std.int(Math.max(Math.min(relativePos.y * -.03, 5), 1)); + } + _scrollVCounter = 0; + } + } - function updatePointerDrag(pointer:FlxPointer):Void + function updatePointerMove(pointer:FlxPointer):Void { if (_selectionIndex < 0) return; @@ -708,6 +769,33 @@ class FlxInputText extends FlxText implements IFlxInputText _pointerCamera = null; } + function updatePointerDoublePress(pointer:FlxPointer):Void + { + var rightPos = text.length; + if (text.length > 0 && _caretIndex >= 0 && rightPos >= _caretIndex) + { + var leftPos = -1; + var pos = 0; + var startPos = FlxMath.maxInt(_caretIndex, 1); + + for (c in DELIMITERS) + { + pos = text.lastIndexOf(c, startPos - 1); + if (pos > leftPos) + leftPos = pos + 1; + + pos = text.indexOf(c, startPos); + if (pos < rightPos && pos != -1) + rightPos = pos; + } + + if (leftPos != rightPos) + { + setSelection(leftPos, rightPos); + } + } + } + function getRelativePosition(pointer:FlxPointer):FlxPoint { var pointerPos = pointer.getWorldPosition(_pointerCamera); @@ -767,7 +855,7 @@ class FlxInputText extends FlxText implements IFlxInputText if (caretColor != value) { caretColor = value; - regenCaret(); + _caret.color = caretColor; } return value; @@ -873,10 +961,6 @@ class FlxInputText extends FlxText implements IFlxInputText if (textField.multiline != value) { textField.multiline = value; - // `wordWrap` will still add new lines even if `multiline` is false, - // let's change it accordingly - wordWrap = value; - _regen = true; } return value; @@ -912,7 +996,6 @@ class FlxInputText extends FlxText implements IFlxInputText { textField.scrollH = value; _regen = true; - updateSelectionSprites(); } return value; } @@ -932,7 +1015,6 @@ class FlxInputText extends FlxText implements IFlxInputText { textField.scrollV = value; _regen = true; - updateSelectionSprites(); } return value; } @@ -962,7 +1044,7 @@ class FlxInputText extends FlxText implements IFlxInputText for (box in _selectionBoxes) { if (box != null) - box.makeGraphic(1, 1, selectionColor); + box.color = selectionColor; } } From 90646809548d15e71630bcb16fddcf75adb98ffb Mon Sep 17 00:00:00 2001 From: Starmapo Date: Fri, 21 Jun 2024 17:53:47 -0400 Subject: [PATCH 11/14] Action callbacks --- flixel/text/FlxInputText.hx | 87 ++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index 4cf87d5024..c04d3f34a5 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -15,12 +15,22 @@ import openfl.utils.QName; class FlxInputText extends FlxText implements IFlxInputText { + public static inline var BACKSPACE_ACTION:String = "backspace"; + + public static inline var DELETE_ACTION:String = "delete"; + + public static inline var ENTER_ACTION:String = "enter"; + + public static inline var INPUT_ACTION:String = "input"; + static inline var GUTTER:Int = 2; static final DELIMITERS:Array = ['\n', '.', '!', '?', ',', ' ', ';', ':', '(', ')', '-', '_', '/']; public var bottomScrollV(get, never):Int; + public var callback:String->String->Void; + public var caretColor(default, set):FlxColor = FlxColor.WHITE; public var caretIndex(get, set):Int; @@ -144,8 +154,8 @@ class FlxInputText extends FlxText implements IFlxInputText { switch (action) { - case ADD_TEXT(text): - replaceSelectedText(text); + case ADD_TEXT(newText): + addText(newText); case MOVE_CURSOR(type, shiftKey): moveCursor(type, shiftKey); case COMMAND(cmd): @@ -163,6 +173,15 @@ class FlxInputText extends FlxText implements IFlxInputText updateSelection(); } + function addText(newText:String):Void + { + newText = filterText(newText); + if (newText.length > 0) + { + replaceSelectedText(newText); + onChange(INPUT_ACTION); + } + } function drawSprite(sprite:FlxSprite):Void { @@ -173,6 +192,25 @@ class FlxInputText extends FlxText implements IFlxInputText sprite.draw(); } } + function filterText(newText:String):String + { + if (maxLength > 0) + { + var removeLength = (selectionEndIndex - selectionBeginIndex); + var newMaxLength = maxLength - text.length + removeLength; + + if (newMaxLength <= 0) + { + newText = ""; + } + else if (newMaxLength < newText.length) + { + newText = newText.substr(0, newMaxLength); + } + } + + return newText; + } function getCharIndexOnDifferentLine(charIndex:Int, lineIndex:Int):Int { @@ -391,6 +429,11 @@ class FlxInputText extends FlxText implements IFlxInputText setSelection(_selectionIndex, _caretIndex); } } + function onChange(action:String):Void + { + if (callback != null) + callback(text, action); + } function regenCaret():Void { @@ -409,21 +452,7 @@ class FlxInputText extends FlxText implements IFlxInputText if (beginIndex == endIndex && maxLength > 0 && text.length == maxLength) return; - - if (beginIndex > text.length) - { - beginIndex = text.length; - } - if (endIndex > text.length) - { - endIndex = text.length; - } - if (endIndex < beginIndex) - { - var cache = endIndex; - endIndex = beginIndex; - beginIndex = cache; - } + if (beginIndex < 0) { beginIndex = 0; @@ -436,22 +465,7 @@ class FlxInputText extends FlxText implements IFlxInputText { if (endIndex < beginIndex || beginIndex < 0 || endIndex > text.length || newText == null) return; - - if (maxLength > 0) - { - var removeLength = (endIndex - beginIndex); - var newMaxLength = maxLength - text.length + removeLength; - - if (newMaxLength <= 0) - { - newText = ""; - } - else if (newMaxLength < newText.length) - { - newText = newText.substr(0, newMaxLength); - } - } - + text = text.substring(0, beginIndex) + newText + text.substring(endIndex); _selectionIndex = _caretIndex = beginIndex + newText.length; @@ -465,8 +479,9 @@ class FlxInputText extends FlxText implements IFlxInputText case NEW_LINE: if (multiline) { - replaceSelectedText("\n"); + addText("\n"); } + onChange(ENTER_ACTION); case DELETE_LEFT: if (_selectionIndex == _caretIndex && _caretIndex > 0) { @@ -477,6 +492,7 @@ class FlxInputText extends FlxText implements IFlxInputText { replaceSelectedText(""); _selectionIndex = _caretIndex; + onChange(BACKSPACE_ACTION); } case DELETE_RIGHT: if (_selectionIndex == _caretIndex && _caretIndex < text.length) @@ -488,6 +504,7 @@ class FlxInputText extends FlxText implements IFlxInputText { replaceSelectedText(""); _selectionIndex = _caretIndex; + onChange(DELETE_ACTION); } case COPY: if (_caretIndex != _selectionIndex && !passwordMode) @@ -504,7 +521,7 @@ class FlxInputText extends FlxText implements IFlxInputText case PASTE: if (Clipboard.text != null) { - replaceSelectedText(Clipboard.text); + addText(Clipboard.text); } case SELECT_ALL: _selectionIndex = 0; From 2257a368c52e0bfe0bc3a3cc310b1998668fa977 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Fri, 21 Jun 2024 18:19:58 -0400 Subject: [PATCH 12/14] Fix "final" keyword screwing up code climate --- checkstyle.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checkstyle.json b/checkstyle.json index b603612dab..60a10dcb3d 100644 --- a/checkstyle.json +++ b/checkstyle.json @@ -35,7 +35,8 @@ "STATIC", "MACRO", "INLINE", - "DYNAMIC" + "DYNAMIC", + "FINAL" ] } }, From 2e043ab72c4806c4e5489f6fb85ffb1b7b52ac0d Mon Sep 17 00:00:00 2001 From: Starmapo Date: Sat, 22 Jun 2024 12:32:39 -0400 Subject: [PATCH 13/14] Various fixes & improvements - Caret is now positioned properly with different alignments - Caret is now clipped inside the text bounds - Caret is now automatically resized when changing `bold`, `font`, `italic`, `size` or `systemFont` variables - Fixed crash when pressing down a key while there isn't a focused input text - Fixed selected text format overwriting the border color - Fixed caret not being visible when text is empty - Fixed selection boxes sometimes not being updated immediately - Added `useSelectedTextFormat` variable - Double press check is now when the mouse is released (same as OpenFL) --- flixel/system/frontEnds/InputTextFrontEnd.hx | 3 + flixel/text/FlxInputText.hx | 151 ++++++++++++++++--- 2 files changed, 133 insertions(+), 21 deletions(-) diff --git a/flixel/system/frontEnds/InputTextFrontEnd.hx b/flixel/system/frontEnds/InputTextFrontEnd.hx index 12ae5327a5..b8530263b3 100644 --- a/flixel/system/frontEnds/InputTextFrontEnd.hx +++ b/flixel/system/frontEnds/InputTextFrontEnd.hx @@ -51,6 +51,9 @@ class InputTextFrontEnd function onKeyDown(key:KeyCode, modifier:KeyModifier) { + if (focus == null) + return; + // Taken from OpenFL's `TextField` var modifierPressed = #if mac modifier.metaKey #elseif js(modifier.metaKey || modifier.ctrlKey) #else (modifier.ctrlKey && !modifier.altKey) #end; diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index c04d3f34a5..3aac6dfddf 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -60,6 +60,14 @@ class FlxInputText extends FlxText implements IFlxInputText public var selectionColor(default, set):FlxColor = FlxColor.BLACK; public var selectionEndIndex(get, never):Int; + + /** + * If `false`, no extra format will be applied for selected text. + * + * Useful if you are using `addFormat()`, as the selected text format might + * overwrite some of their properties. + */ + public var useSelectedTextFormat(default, set):Bool = true; var _caret:FlxSprite; var _caretIndex:Int = -1; @@ -81,9 +89,9 @@ class FlxInputText extends FlxText implements IFlxInputText _selectionFormat.color = selectedTextColor; - _caret = new FlxSprite(); + _caret = new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE); _caret.visible = false; - regenCaret(); + updateCaretSize(); updateCaretPosition(); FlxG.inputText.registerInputText(this); @@ -103,6 +111,8 @@ class FlxInputText extends FlxText implements IFlxInputText override function draw():Void { + regenGraphic(); + for (box in _selectionBoxes) drawSprite(box); @@ -134,7 +144,9 @@ class FlxInputText extends FlxText implements IFlxInputText super.applyFormats(formatAdjusted, useBorderColor); - textField.setTextFormat(_selectionFormat, selectionBeginIndex, selectionEndIndex); + if (!useBorderColor && useSelectedTextFormat) + textField.setTextFormat(_selectionFormat, selectionBeginIndex, selectionEndIndex); + // set the scroll back to how it was scrollH = cacheScrollH; scrollV = cacheScrollV; @@ -173,6 +185,7 @@ class FlxInputText extends FlxText implements IFlxInputText updateSelection(); } + function addText(newText:String):Void { newText = filterText(newText); @@ -192,6 +205,7 @@ class FlxInputText extends FlxText implements IFlxInputText sprite.draw(); } } + function filterText(newText:String):String { if (maxLength > 0) @@ -211,6 +225,16 @@ class FlxInputText extends FlxText implements IFlxInputText return newText; } + + function getCaretOffsetX():Float + { + return switch (alignment) + { + case CENTER: (width / 2); + case RIGHT: width - GUTTER; + default: GUTTER; + } + } function getCharIndexOnDifferentLine(charIndex:Int, lineIndex:Int):Int { @@ -297,6 +321,10 @@ class FlxInputText extends FlxText implements IFlxInputText function isCaretLineVisible():Bool { + // `getLineIndexOfChar()` will return -1 if text is empty, but we still want the caret to show up + if (text.length == 0) + return true; + var line = textField.getLineIndexOfChar(_caretIndex); return line >= scrollV - 1 && line <= bottomScrollV - 1; } @@ -429,16 +457,12 @@ class FlxInputText extends FlxText implements IFlxInputText setSelection(_selectionIndex, _caretIndex); } } + function onChange(action:String):Void { if (callback != null) callback(text, action); } - - function regenCaret():Void - { - _caret.makeGraphic(caretWidth, Std.int(size + 2), FlxColor.WHITE); - } function replaceSelectedText(newText:String):Void { @@ -537,7 +561,7 @@ class FlxInputText extends FlxText implements IFlxInputText if (text.length == 0) { - _caret.setPosition(x + GUTTER, y + GUTTER); + _caret.setPosition(x + getCaretOffsetX(), y + GUTTER); } else { @@ -546,12 +570,31 @@ class FlxInputText extends FlxText implements IFlxInputText { _caret.setPosition(x + boundaries.right - scrollH, y + boundaries.y - getLineY(scrollV - 1)); } - else // end of line + else { - var lineIndex = textField.getLineIndexOfChar(_caretIndex); - _caret.setPosition(x + GUTTER, y + GUTTER + getLineY(lineIndex) - getLineY(scrollV - 1)); + boundaries = textField.getCharBoundaries(_caretIndex); + if (boundaries != null) + { + _caret.setPosition(x + boundaries.x - scrollH, y + boundaries.y - getLineY(scrollV - 1)); + } + else // end of line + { + var lineIndex = textField.getLineIndexOfChar(_caretIndex); + _caret.setPosition(x + getCaretOffsetX(), y + GUTTER + getLineY(lineIndex) - getLineY(scrollV - 1)); + } } } + + _caret.clipRect = _caret.getHitbox(_caret.clipRect).clipTo(FlxRect.weak(x, y, width, height)).offset(-_caret.x, -_caret.y); + } + + function updateCaretSize():Void + { + if (_caret == null) + return; + + _caret.setGraphicSize(caretWidth, textField.getLineMetrics(0).height); + _caret.updateHitbox(); } function updateSelection():Void @@ -660,14 +703,7 @@ class FlxInputText extends FlxText implements IFlxInputText { _mouseDown = false; updatePointerRelease(FlxG.mouse); - } - } - if (checkPointerOverlap(FlxG.mouse)) - { - if (FlxG.mouse.justPressed) - { - _mouseDown = true; - updatePointerPress(FlxG.mouse); + var currentTime = FlxG.game.ticks; if (currentTime - _lastClickTime < 500) { @@ -679,6 +715,14 @@ class FlxInputText extends FlxText implements IFlxInputText _lastClickTime = currentTime; } } + } + if (checkPointerOverlap(FlxG.mouse)) + { + if (FlxG.mouse.justPressed) + { + _mouseDown = true; + updatePointerPress(FlxG.mouse); + } if (FlxG.mouse.wheel != 0) { @@ -722,6 +766,7 @@ class FlxInputText extends FlxText implements IFlxInputText relativePos.put(); } + function updatePointerDrag(pointer:FlxPointer, elapsed:Float) { var relativePos = getRelativePosition(pointer); @@ -823,6 +868,17 @@ class FlxInputText extends FlxText implements IFlxInputText } #end + override function set_bold(value:Bool):Bool + { + if (bold != value) + { + super.set_bold(value); + updateCaretSize(); + } + + return value; + } + override function set_color(value:FlxColor):FlxColor { if (color != value) @@ -833,6 +889,49 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } + override function set_font(value:String):String + { + if (font != value) + { + super.set_font(value); + updateCaretSize(); + } + + return value; + } + + override function set_italic(value:Bool):Bool + { + if (italic != value) + { + super.set_italic(value); + updateCaretSize(); + } + + return value; + } + + override function set_size(value:Int):Int + { + if (size != value) + { + super.set_size(value); + updateCaretSize(); + } + + return value; + } + + override function set_systemFont(value:String):String + { + if (systemFont != value) + { + super.set_systemFont(value); + updateCaretSize(); + } + + return value; + } override function set_text(value:String):String { @@ -903,7 +1002,7 @@ class FlxInputText extends FlxText implements IFlxInputText if (caretWidth != value) { caretWidth = value; - regenCaret(); + updateCaretSize(); } return value; @@ -1072,4 +1171,14 @@ class FlxInputText extends FlxText implements IFlxInputText { return FlxMath.maxInt(_caretIndex, _selectionIndex); } + function set_useSelectedTextFormat(value:Bool):Bool + { + if (useSelectedTextFormat != value) + { + useSelectedTextFormat = value; + _regen = true; + } + + return value; + } } From febc1f395743222f5554de28b24422794fce8d23 Mon Sep 17 00:00:00 2001 From: Starmapo Date: Sat, 22 Jun 2024 15:24:34 -0400 Subject: [PATCH 14/14] Add `forceCase` and filterMode` - Moved action callback types to an enum abstract --- flixel/text/FlxInputText.hx | 117 ++++++++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 17 deletions(-) diff --git a/flixel/text/FlxInputText.hx b/flixel/text/FlxInputText.hx index 3aac6dfddf..b05473d7d0 100644 --- a/flixel/text/FlxInputText.hx +++ b/flixel/text/FlxInputText.hx @@ -15,21 +15,13 @@ import openfl.utils.QName; class FlxInputText extends FlxText implements IFlxInputText { - public static inline var BACKSPACE_ACTION:String = "backspace"; - - public static inline var DELETE_ACTION:String = "delete"; - - public static inline var ENTER_ACTION:String = "enter"; - - public static inline var INPUT_ACTION:String = "input"; - static inline var GUTTER:Int = 2; static final DELIMITERS:Array = ['\n', '.', '!', '?', ',', ' ', ';', ':', '(', ')', '-', '_', '/']; public var bottomScrollV(get, never):Int; - public var callback:String->String->Void; + public var callback:String->FlxInputTextAction->Void; public var caretColor(default, set):FlxColor = FlxColor.WHITE; @@ -37,6 +29,12 @@ class FlxInputText extends FlxText implements IFlxInputText public var caretWidth(default, set):Int = 1; + public var customFilterPattern(default, set):EReg; + + public var filterMode(default, set):FlxInputTextFilterMode = NO_FILTER; + + public var forceCase(default, set):FlxInputTextCase = ALL_CASES; + public var hasFocus(default, set):Bool = false; public var maxLength(default, set):Int = 0; @@ -188,7 +186,7 @@ class FlxInputText extends FlxText implements IFlxInputText function addText(newText:String):Void { - newText = filterText(newText); + newText = filterText(newText, true); if (newText.length > 0) { replaceSelectedText(newText); @@ -206,11 +204,11 @@ class FlxInputText extends FlxText implements IFlxInputText } } - function filterText(newText:String):String + function filterText(newText:String, selection:Bool = false):String { if (maxLength > 0) { - var removeLength = (selectionEndIndex - selectionBeginIndex); + var removeLength = selection ? (selectionEndIndex - selectionBeginIndex) : text.length; var newMaxLength = maxLength - text.length + removeLength; if (newMaxLength <= 0) @@ -223,6 +221,34 @@ class FlxInputText extends FlxText implements IFlxInputText } } + if (forceCase == UPPER_CASE) + { + newText = newText.toUpperCase(); + } + else if (forceCase == LOWER_CASE) + { + newText = newText.toLowerCase(); + } + + if (filterMode != NO_FILTER) + { + var pattern = switch (filterMode) + { + case ONLY_ALPHA: + ~/[^a-zA-Z]*/g; + case ONLY_NUMERIC: + ~/[^0-9]*/g; + case ONLY_ALPHANUMERIC: + ~/[^a-zA-Z0-9]*/g; + case CUSTOM_FILTER: + customFilterPattern; + default: + throw "Unknown filterMode (" + filterMode + ")"; + } + if (pattern != null) + newText = pattern.replace(newText, ""); + } + return newText; } @@ -458,7 +484,7 @@ class FlxInputText extends FlxText implements IFlxInputText } } - function onChange(action:String):Void + function onChange(action:FlxInputTextAction):Void { if (callback != null) callback(text, action); @@ -1008,6 +1034,42 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } + function set_customFilterPattern(value:EReg):EReg + { + if (customFilterPattern != value) + { + customFilterPattern = value; + if (filterMode == CUSTOM_FILTER) + { + text = filterText(text); + } + } + + return value; + } + + function set_filterMode(value:FlxInputTextFilterMode):FlxInputTextFilterMode + { + if (filterMode != value) + { + filterMode = value; + text = filterText(text); + } + + return value; + } + + function set_forceCase(value:FlxInputTextCase):FlxInputTextCase + { + if (forceCase != value) + { + forceCase = value; + text = filterText(text); + } + + return value; + } + function set_hasFocus(value:Bool):Bool { if (hasFocus != value) @@ -1048,10 +1110,7 @@ class FlxInputText extends FlxText implements IFlxInputText if (maxLength != value) { maxLength = value; - if (maxLength > 0 && text.length > maxLength) - { - text = text.substr(0, maxLength); - } + text = filterText(text); } return value; @@ -1182,3 +1241,27 @@ class FlxInputText extends FlxText implements IFlxInputText return value; } } + +enum abstract FlxInputTextAction(String) from String to String +{ + var INPUT_ACTION = "input"; + var BACKSPACE_ACTION = "backspace"; + var DELETE_ACTION = "delete"; + var ENTER_ACTION = "enter"; +} + +enum abstract FlxInputTextCase(Int) from Int to Int +{ + var ALL_CASES = 0; + var UPPER_CASE = 1; + var LOWER_CASE = 2; +} + +enum abstract FlxInputTextFilterMode(Int) from Int to Int +{ + var NO_FILTER = 0; + var ONLY_ALPHA = 1; + var ONLY_NUMERIC = 2; + var ONLY_ALPHANUMERIC = 3; + var CUSTOM_FILTER = 4; +} \ No newline at end of file