blob: 4e4132f2b65157c717d1d3b57b1c1b91a6f47e18 [file]
#include "rive/text/text_input.hpp"
#include "rive/text/text_style.hpp"
#include "rive/text/text_input_drawable.hpp"
#include "rive/math/mat2d.hpp"
#include "rive/artboard.hpp"
#include "rive/factory.hpp"
#include "rive/constraints/scrolling/scroll_constraint.hpp"
#include <algorithm>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#endif
using namespace rive;
void TextInput::draw(Renderer* renderer) {}
Core* TextInput::hitTest(HitInfo*, const Mat2D&) { return nullptr; }
bool TextInput::hitTestPoint(const Vec2D& position,
bool skipOnUnclipped,
bool isPrimaryHit)
{
// TextInput must check its own bounds before delegating to parent.
// Unlike layouts, we always check bounds regardless of clip state.
Mat2D inverseWorld;
if (!worldTransform().invert(&inverseWorld))
{
return false;
}
Vec2D localPosition = inverseWorld * position;
if (!localBounds().contains(localPosition))
{
return false;
}
// Bounds check passed, now check parent hierarchy
return Drawable::hitTestPoint(position, skipOnUnclipped, isPrimaryHit);
}
void TextInput::textChanged()
{
m_sourceText = m_Text;
#ifdef WITH_RIVE_TEXT
syncDisplayedTextFromSource(false);
#endif
#ifdef WITH_RIVE_LAYOUT
markLayoutNodeDirty();
#endif
markShapeDirty();
}
void TextInput::selectionRadiusChanged()
{
#ifdef WITH_RIVE_TEXT
m_rawTextInput.selectionCornerRadius(selectionRadius());
markShapeDirty();
#endif
}
void TextInput::multilineChanged() { updateMultiline(true); }
void TextInput::markPaintDirty() { addDirt(ComponentDirt::Paint); }
void TextInput::markShapeDirty() { addDirt(ComponentDirt::TextShape); }
AABB TextInput::localBounds() const
{
#ifdef WITH_RIVE_TEXT
return m_rawTextInput.bounds();
#else
return AABB();
#endif
}
StatusCode TextInput::onAddedClean(CoreContext* context)
{
Super::onAddedClean(context);
m_textStyle = children<TextStyle>().first();
m_sourceText = m_Text;
#ifdef WITH_RIVE_TEXT
if (m_textStyle != nullptr && m_textStyle->font() != nullptr)
{
m_rawTextInput.font(m_textStyle->font());
m_rawTextInput.fontSize(m_textStyle->fontSize());
}
syncDisplayedTextFromSource(false);
#endif
if (parent() != nullptr)
{
auto parentParent = parent()->parent();
if (parentParent != nullptr && parentParent->is<TransformComponent>())
{
for (Constraint* constraint :
parentParent->as<TransformComponent>()->constraints())
{
if (constraint->is<ScrollConstraint>())
{
m_scrollConstraint = constraint->as<ScrollConstraint>();
break;
}
}
}
}
updateMultiline();
return m_textStyle == nullptr ? StatusCode::MissingObject : StatusCode::Ok;
}
void TextInput::update(ComponentDirt value)
{
Super::update(value);
#ifdef WITH_RIVE_TEXT
if (hasDirt(value, ComponentDirt::Paint | ComponentDirt::TextShape))
{
Factory* factory = artboard()->factory();
m_rawTextInput.fontSize(m_textStyle->fontSize());
RawTextInput::Flags changed = m_rawTextInput.update(factory);
if (enums::is_flag_set(changed, RawTextInput::Flags::shapeDirty))
{
m_worldBounds =
worldTransform().mapBoundingBox(m_rawTextInput.bounds());
#ifdef WITH_RIVE_LAYOUT
if (m_rawTextInput.sizing() == TextSizing::autoHeight)
{
markLayoutNodeDirty();
}
#endif
}
if (enums::is_flag_set(changed, RawTextInput::Flags::selectionDirty))
{
for (auto child : children<TextInputDrawable>())
{
child->invalidateStrokeEffects();
}
}
// Skip scroll adjustment during drag - edge scrolling handles it.
if (m_scrollX == 0.0f && m_scrollY == 0.0f &&
m_scrollConstraint != nullptr && !m_isDragging)
{
CursorVisualPosition cursorPosition =
m_rawTextInput.cursorVisualPosition();
float viewportWidth = m_scrollConstraint->viewportWidth();
float viewportHeight = m_scrollConstraint->viewportHeight();
const float cursorWidth = 1.0f;
bool useHorizontalScroll =
!multiline() && m_scrollConstraint->constrainsHorizontal();
bool useVerticalScroll =
multiline() && m_scrollConstraint->constrainsVertical();
float viewportX =
cursorPosition.x() + m_scrollConstraint->scrollOffsetX();
float viewportTop =
cursorPosition.top() + m_scrollConstraint->scrollOffsetY();
float viewportBottom =
cursorPosition.bottom() + m_scrollConstraint->scrollOffsetY();
if (useHorizontalScroll && viewportX < 0.0f)
{
m_scrollConstraint->stopPhysics();
float scrollOffset =
m_scrollConstraint->scrollOffsetX() - viewportX;
m_scrollConstraint->scrollOffsetX(scrollOffset);
}
else if (useHorizontalScroll &&
viewportX > viewportWidth - cursorWidth)
{
m_scrollConstraint->stopPhysics();
float scrollOffset = m_scrollConstraint->scrollOffsetX() -
(viewportX - viewportWidth + cursorWidth);
m_scrollConstraint->scrollOffsetX(scrollOffset);
}
if (useVerticalScroll && viewportTop < 0.0f)
{
m_scrollConstraint->stopPhysics();
float scrollOffset =
m_scrollConstraint->scrollOffsetY() - viewportTop;
m_scrollConstraint->scrollOffsetY(scrollOffset);
}
else if (useVerticalScroll && viewportBottom > viewportHeight)
{
m_scrollConstraint->stopPhysics();
float scrollOffset = m_scrollConstraint->scrollOffsetY() -
(viewportBottom - viewportHeight);
m_scrollConstraint->scrollOffsetY(scrollOffset);
}
}
}
#endif
}
Vec2D TextInput::measureLayout(float width,
LayoutMeasureMode widthMode,
float height,
LayoutMeasureMode heightMode)
{
#ifdef WITH_RIVE_TEXT
AABB bounds =
m_rawTextInput.measure(widthMode == LayoutMeasureMode::undefined
? std::numeric_limits<float>::max()
: width,
heightMode == LayoutMeasureMode::undefined
? std::numeric_limits<float>::max()
: height);
return bounds.size();
#else
return Vec2D();
#endif
}
void TextInput::controlSize(Vec2D size,
LayoutScaleType widthScaleType,
LayoutScaleType heightScaleType,
LayoutDirection direction)
{
m_layoutWidth = size.x;
updateMultiline();
}
std::string TextInput::strippedLineBreaks(const std::string& text)
{
std::string stripped;
stripped.reserve(text.size());
bool inLineBreak = false;
for (char c : text)
{
if (c == '\n' || c == '\r')
{
if (!inLineBreak)
{
stripped.push_back(' ');
inLineBreak = true;
}
}
else
{
stripped.push_back(c);
inLineBreak = false;
}
}
return stripped;
}
std::string TextInput::displayedText() const
{
return multiline() ? m_sourceText : strippedLineBreaks(m_sourceText);
}
void TextInput::syncDisplayedTextFromSource(bool preserveCursor)
{
#ifdef WITH_RIVE_TEXT
std::string nextDisplayText = displayedText();
if (m_rawTextInput.text() == nextDisplayText)
{
return;
}
if (preserveCursor)
{
m_rawTextInput.textPreserveCursor(nextDisplayText);
}
else
{
m_rawTextInput.text(nextDisplayText);
}
#endif
}
void TextInput::syncSourceTextFromRaw()
{
#ifdef WITH_RIVE_TEXT
std::string rawText = m_rawTextInput.text();
if (!multiline())
{
std::string singleLineText = strippedLineBreaks(rawText);
if (singleLineText != rawText)
{
m_rawTextInput.textPreserveCursor(singleLineText);
rawText = std::move(singleLineText);
}
}
m_sourceText = std::move(rawText);
text(m_sourceText);
#endif
}
#ifdef WITH_RIVE_TEXT
static CursorBoundary cursorBoundary(KeyModifiers modifiers)
{
if ((modifiers & KeyModifiers::meta) != KeyModifiers::none)
{
return CursorBoundary::line;
}
if ((modifiers & KeyModifiers::alt) != KeyModifiers::none)
{
if ((modifiers & KeyModifiers::ctrl) != KeyModifiers::none)
{
return CursorBoundary::subWord;
}
return CursorBoundary::word;
}
return CursorBoundary::character;
}
#if defined(__EMSCRIPTEN__)
EM_JS(bool, isWindowsBrowser, (), {
return Boolean(navigator.platform.indexOf('Win') > -1);
});
#endif
static KeyModifiers systemModifier()
{
#if defined(__EMSCRIPTEN__)
return isWindowsBrowser() ? KeyModifiers::ctrl : KeyModifiers::meta;
#elif defined(RIVE_WINDOWS)
return KeyModifiers::ctrl;
#else
return KeyModifiers::meta;
#endif
}
#endif
void TextInput::updateMultiline(bool syncDisplayedText)
{
#ifdef WITH_RIVE_TEXT
if (multiline())
{
m_rawTextInput.maxWidth(m_layoutWidth);
m_rawTextInput.sizing(TextSizing::autoHeight);
}
else
{
m_rawTextInput.maxWidth(0);
m_rawTextInput.sizing(TextSizing::autoWidth);
}
if (m_scrollConstraint != nullptr)
{
if (multiline() && m_scrollConstraint->scrollOffsetX() != 0.0f)
{
m_scrollConstraint->stopPhysics();
m_scrollConstraint->scrollOffsetX(0.0f);
}
else if (!multiline() && m_scrollConstraint->scrollOffsetY() != 0.0f)
{
m_scrollConstraint->stopPhysics();
m_scrollConstraint->scrollOffsetY(0.0f);
}
}
if (syncDisplayedText)
{
syncDisplayedTextFromSource(true);
}
#ifdef WITH_RIVE_LAYOUT
markLayoutNodeDirty();
#endif
addDirt(ComponentDirt::TextShape);
#endif
}
bool TextInput::keyInput(Key value,
KeyModifiers modifiers,
bool isPressed,
bool isRepeat)
{
#ifdef WITH_RIVE_TEXT
if (isPressed)
{
switch (value)
{
case Key::z:
if ((modifiers & (systemModifier() | KeyModifiers::shift)) ==
(systemModifier() | KeyModifiers::shift))
{
m_rawTextInput.redo();
syncSourceTextFromRaw();
markShapeDirty();
return true;
}
else if ((modifiers & systemModifier()) != KeyModifiers::none)
{
m_rawTextInput.undo();
syncSourceTextFromRaw();
markShapeDirty();
return true;
}
break;
case Key::backspace:
m_rawTextInput.backspace(-1);
syncSourceTextFromRaw();
markShapeDirty();
return true;
case Key::deleteKey:
m_rawTextInput.backspace(1);
syncSourceTextFromRaw();
markShapeDirty();
return true;
case Key::left:
m_rawTextInput.cursorLeft(cursorBoundary(modifiers),
(modifiers & KeyModifiers::shift) !=
KeyModifiers::none);
markPaintDirty();
return true;
case Key::right:
m_rawTextInput.cursorRight(cursorBoundary(modifiers),
(modifiers & KeyModifiers::shift) !=
KeyModifiers::none);
markPaintDirty();
return true;
case Key::up:
m_rawTextInput.cursorUp((modifiers & KeyModifiers::shift) !=
KeyModifiers::none);
markPaintDirty();
return true;
case Key::down:
m_rawTextInput.cursorDown((modifiers & KeyModifiers::shift) !=
KeyModifiers::none);
markPaintDirty();
return true;
case Key::enter:
if (!multiline())
{
return false;
}
m_rawTextInput.insert("\n");
syncSourceTextFromRaw();
markPaintDirty();
return true;
default:
return false;
}
}
#endif
return false;
}
bool TextInput::textInput(const std::string& value)
{
#ifdef WITH_RIVE_TEXT
std::string textToInsert = multiline() ? value : strippedLineBreaks(value);
if (textToInsert.empty())
{
return true;
}
m_rawTextInput.insert(textToInsert);
syncSourceTextFromRaw();
markShapeDirty();
#endif
return true;
}
void TextInput::focused()
{
// TODO: Implement focus visual feedback
}
void TextInput::blurred()
{
// TODO: Implement blur visual feedback
}
bool TextInput::worldPosition(Vec2D& outPosition)
{
// TextInput is a TransformComponent, so we can access worldTransform
// directly
Vec2D localPos = worldTranslation();
// Transform to root artboard space (handles nested artboards)
auto* ab = artboard();
if (ab)
{
outPosition = ab->rootTransform(localPos);
}
else
{
outPosition = localPos;
}
return true;
}
bool TextInput::worldBounds(AABB& outBounds)
{
// m_worldBounds is computed in update() via
// worldTransform().mapBoundingBox()
if (m_worldBounds.isEmptyOrNaN())
{
return false;
}
// Transform to root artboard space (handles nested artboards)
auto* ab = artboard();
if (ab)
{
Vec2D minPt = ab->rootTransform(m_worldBounds.min());
Vec2D maxPt = ab->rootTransform(m_worldBounds.max());
outBounds = AABB(minPt, maxPt);
}
else
{
outBounds = m_worldBounds;
}
return true;
}
float TextInput::edgeScrollSpeedForDistance(float distanceFromEdge) const
{
static constexpr float kEdgeScrollBaseSpeed = 45.0f;
static constexpr float kEdgeScrollMaxSpeed = 400.0f;
static constexpr float kEdgeScrollSpeedRamp = 4.0f;
float speed =
kEdgeScrollBaseSpeed + distanceFromEdge * kEdgeScrollSpeedRamp;
return std::max(kEdgeScrollBaseSpeed, std::min(kEdgeScrollMaxSpeed, speed));
}
float TextInput::edgeActivationDistance(float position, float edgeStart) const
{
return position >= edgeStart ? 0.0f : edgeStart - position;
}
bool TextInput::worldToLocalWithViewport(Vec2D worldPosition,
Vec2D& outLocal,
bool enableAutoScroll)
{
m_scrollX = 0.0f;
m_scrollY = 0.0f;
// Get the inverse of the world transform to convert to local coordinates
Mat2D inverseWorld;
if (!worldTransform().invert(&inverseWorld))
{
return false;
}
Vec2D localPos = inverseWorld * worldPosition;
// If we have a scroll constraint, handle viewport clamping and edge
// detection.
if (m_scrollConstraint != nullptr)
{
float viewportWidth = m_scrollConstraint->viewportWidth();
float viewportHeight = m_scrollConstraint->viewportHeight();
float scrollOffsetX = m_scrollConstraint->scrollOffsetX();
float scrollOffsetY = m_scrollConstraint->scrollOffsetY();
const float edgeThreshold = 20.0f;
bool useHorizontalScroll =
!multiline() && m_scrollConstraint->constrainsHorizontal();
bool useVerticalScroll =
multiline() && m_scrollConstraint->constrainsVertical();
if (useHorizontalScroll)
{
// Convert to viewport-relative coordinates.
float viewportX = localPos.x + scrollOffsetX;
float leftDistance =
edgeActivationDistance(viewportX, edgeThreshold);
float rightDistance =
edgeActivationDistance(viewportWidth - viewportX,
edgeThreshold);
// Edge detection for auto-scrolling.
if (enableAutoScroll && leftDistance > 0.0f)
{
m_scrollX = edgeScrollSpeedForDistance(leftDistance);
if (viewportX < 0.0f)
{
localPos.x = -scrollOffsetX;
}
}
else if (enableAutoScroll && rightDistance > 0.0f)
{
m_scrollX = -edgeScrollSpeedForDistance(rightDistance);
if (viewportX > viewportWidth)
{
localPos.x = viewportWidth - scrollOffsetX;
}
}
}
if (useVerticalScroll)
{
// Convert to viewport-relative coordinates.
float viewportY = localPos.y + scrollOffsetY;
float topDistance =
edgeActivationDistance(viewportY, edgeThreshold);
float bottomDistance =
edgeActivationDistance(viewportHeight - viewportY,
edgeThreshold);
// Edge detection for auto-scrolling.
if (enableAutoScroll && topDistance > 0.0f)
{
m_scrollY = edgeScrollSpeedForDistance(topDistance);
if (viewportY < 0.0f)
{
localPos.y = -scrollOffsetY;
}
}
else if (enableAutoScroll && bottomDistance > 0.0f)
{
m_scrollY = -edgeScrollSpeedForDistance(bottomDistance);
if (viewportY > viewportHeight)
{
localPos.y = viewportHeight - scrollOffsetY;
}
}
}
}
outLocal = localPos;
return true;
}
void TextInput::startDrag(Vec2D worldPosition)
{
#ifdef WITH_RIVE_TEXT
m_isDragging = true;
m_lastDragWorldPosition = worldPosition;
Vec2D localPos;
if (!worldToLocalWithViewport(worldPosition, localPos, false))
{
return;
}
m_rawTextInput.moveCursorTo(localPos, false);
markPaintDirty();
// Note: Focus is handled by TextInputListenerGroup which can cross
// artboard boundaries to find the appropriate FocusData.
#endif
}
void TextInput::drag(Vec2D worldPosition)
{
#ifdef WITH_RIVE_TEXT
m_lastDragWorldPosition = worldPosition;
Vec2D localPos;
if (!worldToLocalWithViewport(worldPosition, localPos, true))
{
return;
}
m_rawTextInput.moveCursorTo(localPos, true);
markPaintDirty();
#endif
}
void TextInput::endDrag(Vec2D worldPosition)
{
m_isDragging = false;
m_lastDragWorldPosition = Vec2D(NAN, NAN);
m_scrollX = 0.0f;
m_scrollY = 0.0f;
}
bool TextInput::advanceDrag(float elapsedSeconds)
{
#ifdef WITH_RIVE_TEXT
if (!m_isDragging)
{
m_scrollX = 0.0f;
m_scrollY = 0.0f;
return false;
}
if ((m_scrollX == 0.0f && m_scrollY == 0.0f) ||
m_scrollConstraint == nullptr)
{
return false;
}
m_scrollConstraint->stopPhysics();
if (m_scrollX != 0.0f)
{
float scrollDeltaX = m_scrollX * elapsedSeconds;
float newScrollOffsetX =
m_scrollConstraint->scrollOffsetX() + scrollDeltaX;
if (!m_scrollConstraint->infinite())
{
newScrollOffsetX = std::max(m_scrollConstraint->maxOffsetX(),
std::min(0.0f, newScrollOffsetX));
}
m_scrollConstraint->scrollOffsetX(newScrollOffsetX);
}
if (m_scrollY != 0.0f)
{
float scrollDeltaY = m_scrollY * elapsedSeconds;
float newScrollOffsetY =
m_scrollConstraint->scrollOffsetY() + scrollDeltaY;
if (!m_scrollConstraint->infinite())
{
newScrollOffsetY = std::max(m_scrollConstraint->maxOffsetY(),
std::min(0.0f, newScrollOffsetY));
}
m_scrollConstraint->scrollOffsetY(newScrollOffsetY);
}
// Keep selection/caret in sync while edge-scrolling even if pointer isn't
// moving.
if (std::isfinite(m_lastDragWorldPosition.x) &&
std::isfinite(m_lastDragWorldPosition.y))
{
Vec2D localPos;
if (worldToLocalWithViewport(m_lastDragWorldPosition, localPos, true))
{
m_rawTextInput.moveCursorTo(localPos, true);
markPaintDirty();
}
}
// Continue scrolling while still dragging
return m_isDragging;
#else
return false;
#endif
}
bool TextInput::advanceComponent(float elapsedSeconds, AdvanceFlags flags)
{
return advanceDrag(elapsedSeconds);
}