Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 85 additions & 17 deletions flixel/text/FlxInputText.hx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,22 @@ class FlxInputText extends FlxText implements IFlxInputText
*/
var _justGainedFocus:Bool = false;

/**
* Stores the origin that sprites were last positioned with, used for checking
* if they need to be updated.
*/
var _lastOrigin:FlxPoint = FlxPoint.get();
/**
* Stores the offset that sprites were last positioned with, used for checking
* if they need to be updated.
*/
var _lastOffset:FlxPoint = FlxPoint.get();
/**
* Stores the scale that sprites were last positioned with, used for checking
* if they need to be updated.
*/
var _lastScale:FlxPoint = FlxPoint.get(1, 1);

/**
* Internal variable that holds the camera that the text field is being pressed on.
*/
Expand Down Expand Up @@ -466,6 +482,14 @@ class FlxInputText extends FlxText implements IFlxInputText
{
regenGraphic();

if (!_lastScale.equals(scale) || !_lastOrigin.equals(origin) || !_lastOffset.equals(offset))
{
_lastScale.copyFrom(scale);
_lastOrigin.copyFrom(origin);
_lastOffset.copyFrom(offset);
updateSpritePositions();
}

drawSprite(_fieldBorderSprite);
drawSprite(_backgroundSprite);

Expand Down Expand Up @@ -493,6 +517,9 @@ class FlxInputText extends FlxText implements IFlxInputText
_backgroundSprite = FlxDestroyUtil.destroy(_backgroundSprite);
_caret = FlxDestroyUtil.destroy(_caret);
_fieldBorderSprite = FlxDestroyUtil.destroy(_fieldBorderSprite);
_lastOffset = FlxDestroyUtil.put(_lastOffset);
_lastOrigin = FlxDestroyUtil.put(_lastOrigin);
_lastScale = FlxDestroyUtil.put(_lastScale);
_pointerCamera = null;
while (_selectionBoxes.length > 0)
FlxDestroyUtil.destroy(_selectionBoxes.pop());
Expand Down Expand Up @@ -631,7 +658,7 @@ class FlxInputText extends FlxText implements IFlxInputText
{
bounds = bounds.clipTo(clipRect);
}
bounds.offset(x - sprite.x, y - sprite.y);
bounds.offset(-worldToFrameX(sprite.x), -worldToFrameY(sprite.y));

sprite.clipRect = rect.clipTo(bounds);

Expand All @@ -646,11 +673,49 @@ class FlxInputText extends FlxText implements IFlxInputText
if (sprite != null && sprite.visible)
{
sprite.scrollFactor.copyFrom(scrollFactor);
sprite.scale.copyFrom(scale);
sprite.origin.set(0, 0);

@Geokureli Geokureli Oct 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should assume 0 origin, here. This change only works if the dev explicitly sets the origin to zero, otherwise the text is misaligned. while it may be common for devs to set the origin to zero, it's not the default.

I see 3 options:

  1. Automatically set FlxInputText's origin to 0. this is in line with how people typically use input text
  2. Change this line to sprite.origin.copyFrom(origin); and better account for non-zero origin in the text selection code
  3. No change to this PR, require the dev to set origin to zero when scaling

Number 1 could break existing code (while unlikely, we would still need to wait for the next major release rather than a patch release.) Number 2 is the most robust and ideal, but I don't know the work involved. Number 3 is clearly the easiest

@Geokureli Geokureli Oct 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have similar thoughts for offset, it looks like the offset is used to position the sub sprites rather than being copied to them. I wonder if that plays well with things like clipRect and getPixelAt (though I don't know if this is something we tested before). Worth noting that without testing the features mentioned above, offsetting the input text behaves exactly how I expect

I'm okay with also leaving this as is, for the scope of this, but I do find it odd that scale is copied to child sprites, offset is not and origin is assumed zero. Three different solutions for three the three properties.

sprite._cameras = _cameras;
sprite.draw();
}
}

/**
* Gets the X position in the world of the specified X position in the text field,
* takes `scale`, `origin` and `offset` into account.
*/
inline function frameToWorldX(frameX:Float):Float
{
return x - offset.x + origin.x + (frameX - origin.x) * scale.x;
}

/**
* Gets the Y position in the world of the specified Y position in the text field,
* takes `scale`, `origin` and `offset` into account.
*/
inline function frameToWorldY(frameY:Float):Float
{
return y - offset.y + origin.y + (frameY - origin.y) * scale.y;
}

/**
* Gets the X position in the text field of the specified X position in the world,
* takes `scale`, `origin` and `offset` into account.
*/
inline function worldToFrameX(worldX:Float):Float
{
return (worldX - x + offset.x - origin.x) / scale.x + origin.x;
}

/**
* Gets the Y position in the text field of the specified Y position in the world,
* takes `scale`, `origin` and `offset` into account.
*/
inline function worldToFrameY(worldY:Float):Float
{
return (worldY - y + offset.y - origin.y) / scale.y + origin.y;
}
Comment on lines +692 to +717

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlxSprite has a few helpers similar to this, but it doesn't have simple X/Y converters. it may be worth adding them

For instance: worldToFramePositionSimple accounts for scale, origin, offset, rotation, flipX/Y as well well little known frame properties like frame offset and rotation. worldToFramePosition accounts for even more things like scrollFactor

All of this is outside the scope of this PR but I wanna note it, and make a new issue about it after this is merged


/**
* Returns the specified text filtered using `forceCase` and `filterMode`.
* @param newText The string to filter.
Expand Down Expand Up @@ -1200,8 +1265,8 @@ class FlxInputText extends FlxText implements IFlxInputText
if (!background)
return;

_fieldBorderSprite.setPosition(x - fieldBorderThickness, y - fieldBorderThickness);
_backgroundSprite.setPosition(x, y);
_fieldBorderSprite.setPosition(frameToWorldX(-fieldBorderThickness), frameToWorldY(-fieldBorderThickness));
_backgroundSprite.setPosition(frameToWorldX(0), frameToWorldY(0));
clipSprite(_fieldBorderSprite, true);
clipSprite(_backgroundSprite);
}
Expand All @@ -1216,25 +1281,25 @@ class FlxInputText extends FlxText implements IFlxInputText

if (text.length == 0)
{
_caret.setPosition(x + getCaretOffsetX(), y + GUTTER);
_caret.setPosition(frameToWorldX(getCaretOffsetX()), frameToWorldY(GUTTER));
}
else
{
var boundaries = getCharBoundaries(_caretIndex - 1);
if (boundaries != null)
{
_caret.setPosition(x + boundaries.right - scrollH, y + boundaries.y - getScrollVOffset());
_caret.setPosition(frameToWorldX(boundaries.right - scrollH), frameToWorldY(boundaries.y - getScrollVOffset()));
}
else
{
boundaries = getCharBoundaries(_caretIndex);
if (boundaries != null)
{
_caret.setPosition(x + boundaries.x - scrollH, y + boundaries.y - getScrollVOffset());
_caret.setPosition(frameToWorldX(boundaries.x - scrollH), frameToWorldY(boundaries.y - getScrollVOffset()));
}
else // end of line
{
_caret.setPosition(x + getCaretOffsetX(), y + GUTTER + getLineY(getLineIndexOfChar(_caretIndex)) - getScrollVOffset());
_caret.setPosition(frameToWorldX(getCaretOffsetX()), frameToWorldY(GUTTER + getLineY(getLineIndexOfChar(_caretIndex)) - getScrollVOffset()));
}
}
}
Expand Down Expand Up @@ -1416,7 +1481,7 @@ class FlxInputText extends FlxText implements IFlxInputText
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.setPosition(frameToWorldX(boxRect.x), frameToWorldY(boxRect.y));
box.makeGraphic(Std.int(boxRect.width), Std.int(boxRect.height));
clipSprite(box);
box.visible = true;
Expand Down Expand Up @@ -1572,11 +1637,12 @@ class FlxInputText extends FlxText implements IFlxInputText
function checkPointerOverlap(pointer:FlxPointer):Bool
{
var overlap = false;
var pointerPos = FlxPoint.get();
for (camera in getCameras())
{
pointer.getWorldPosition(camera, pointerPos);
if (overlapsPoint(pointerPos, true, camera))
var relativePos = getRelativePosition(pointer, camera);
var pointerOverlaps = relativePos.x >= 0 && relativePos.x < width && relativePos.y >= 0 && relativePos.y < height;
relativePos.put();
if (pointerOverlaps)
{
if (_pointerCamera == null)
_pointerCamera = camera;
Expand All @@ -1585,7 +1651,6 @@ class FlxInputText extends FlxText implements IFlxInputText
}
}

pointerPos.put();
return overlap;
}

Expand Down Expand Up @@ -1722,13 +1787,16 @@ class FlxInputText extends FlxText implements IFlxInputText
}

/**
* Returns the position of the pointer relative to the text field.
* Returns the position of the pointer relative to the text field, using
* `_pointerCamera` if `camera` is `null`.
*/
function getRelativePosition(pointer:FlxPointer):FlxPoint
function getRelativePosition(pointer:FlxPointer, ?camera:FlxCamera):FlxPoint
{
var pointerPos = pointer.getWorldPosition(_pointerCamera, FlxPoint.get());
getScreenPosition(_point, _pointerCamera);
var result = FlxPoint.get((pointerPos.x - _pointerCamera.scroll.x) - _point.x, (pointerPos.y - _pointerCamera.scroll.y) - _point.y);
if (camera == null)
camera = _pointerCamera;

var pointerPos = pointer.getWorldPosition(camera, FlxPoint.get());
var result = transformWorldToPixels(pointerPos, camera);
pointerPos.put();
return result;
}
Expand Down
Loading