blob: d4a34004c6ffce1fb31deae3b576ef66c58d9edf [file] [log] [blame]
#ifdef WITH_RIVE_TEXT
#include "rive/text/cursor.hpp"
#include "rive/text/font_hb.hpp"
#include "rive/text/text_input.hpp"
#include "rive/text/text_input_drawable.hpp"
#include "rive/text/text_input_text.hpp"
#include "rive/text/text_input_cursor.hpp"
#include "rive/text/text_input_selection.hpp"
#include "rive/text/text_input_selected_text.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/input/focusable.hpp"
#include "rive_testing.hpp"
#include "utils/no_op_factory.hpp"
#include "rive_file_reader.hpp"
#include "utils/serializing_factory.hpp"
using namespace rive;
TEST_CASE("file with text input loads correctly", "[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
CHECK(file != nullptr);
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard->objects<TextInput>().size() == 1);
auto textInput = artboard->objects<TextInput>().first();
CHECK(textInput != nullptr);
CHECK(textInput->children<TextInputDrawable>().size() == 4);
CHECK(textInput->children<TextInputText>().size() == 1);
CHECK(textInput->children<TextInputSelection>().size() == 1);
CHECK(textInput->children<TextInputCursor>().size() == 1);
CHECK(textInput->children<TextInputSelectedText>().size() == 1);
}
TEST_CASE("file with text input renders correctly", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/text_input.riv", &silver);
auto artboard = file->artboardNamed("Text Input - Multiline");
silver.frameSize(artboard->width(), artboard->height());
artboard->advance(0.0f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
CHECK(silver.matches("text_input"));
}
TEST_CASE("text input keyInput handles arrow keys", "[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard != nullptr);
auto textInput = artboard->objects<TextInput>().first();
CHECK(textInput != nullptr);
// Set some initial text
textInput->rawTextInput()->text("hello world");
textInput->rawTextInput()->cursor(Cursor::zero());
artboard->advance(0.0f);
// Test right arrow key
bool handled =
textInput->keyInput(Key::right, KeyModifiers::none, true, false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->cursor().start().codePointIndex() == 1);
// Test left arrow key
handled = textInput->keyInput(Key::left, KeyModifiers::none, true, false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->cursor().start().codePointIndex() == 0);
// Test down arrow key (moves to end on single line)
handled = textInput->keyInput(Key::down, KeyModifiers::none, true, false);
CHECK(handled == true);
// Test up arrow key (moves to start on single line)
handled = textInput->keyInput(Key::up, KeyModifiers::none, true, false);
CHECK(handled == true);
}
TEST_CASE("text input keyInput handles backspace and delete", "[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard != nullptr);
auto textInput = artboard->objects<TextInput>().first();
CHECK(textInput != nullptr);
// Set some initial text
textInput->rawTextInput()->text("hello");
textInput->rawTextInput()->cursor(
Cursor::collapsed(CursorPosition(3))); // "hel|lo"
artboard->advance(0.0f);
// Test backspace
bool handled =
textInput->keyInput(Key::backspace, KeyModifiers::none, true, false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->text() == "helo");
// Test delete key
textInput->rawTextInput()->cursor(
Cursor::collapsed(CursorPosition(2))); // "he|lo"
handled =
textInput->keyInput(Key::deleteKey, KeyModifiers::none, true, false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->text() == "heo");
}
TEST_CASE("text input keyInput handles undo/redo", "[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard != nullptr);
auto textInput = artboard->objects<TextInput>().first();
CHECK(textInput != nullptr);
// Set some initial text and make changes
textInput->rawTextInput()->text("");
textInput->rawTextInput()->cursor(Cursor::zero());
textInput->rawTextInput()->insert("hello");
artboard->advance(0.0f);
CHECK(textInput->rawTextInput()->text() == "hello");
// Test undo with system modifier (meta on macOS/Linux, ctrl on Windows)
// On non-Windows, non-Emscripten, systemModifier() returns
// KeyModifiers::meta
#if !defined(RIVE_WINDOWS) && !defined(__EMSCRIPTEN__)
bool handled = textInput->keyInput(Key::z, KeyModifiers::meta, true, false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->text() == "");
// Insert again and test redo
textInput->rawTextInput()->insert("world");
artboard->advance(0.0f);
CHECK(textInput->rawTextInput()->text() == "world");
// Undo
handled = textInput->keyInput(Key::z, KeyModifiers::meta, true, false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->text() == "");
// Redo with shift+meta
handled = textInput->keyInput(Key::z,
KeyModifiers::meta | KeyModifiers::shift,
true,
false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->text() == "world");
#endif
}
TEST_CASE("text input keyInput returns false for unhandled keys",
"[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard != nullptr);
auto textInput = artboard->objects<TextInput>().first();
CHECK(textInput != nullptr);
artboard->advance(0.0f);
// Test unhandled key
bool handled =
textInput->keyInput(Key::escape, KeyModifiers::none, true, false);
CHECK(handled == false);
// Test that key release (isPressed=false) returns false
handled = textInput->keyInput(Key::right, KeyModifiers::none, false, false);
CHECK(handled == false);
}
TEST_CASE("text input keyInput handles modifier keys for cursor boundary",
"[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard != nullptr);
auto textInput = artboard->objects<TextInput>().first();
CHECK(textInput != nullptr);
// Set text with multiple words
textInput->rawTextInput()->text("one two three");
textInput->rawTextInput()->cursor(Cursor::zero());
artboard->advance(0.0f);
// Test word boundary with alt modifier
bool handled =
textInput->keyInput(Key::right, KeyModifiers::alt, true, false);
CHECK(handled == true);
// Should jump to end of "one"
CHECK(textInput->rawTextInput()->cursor().start().codePointIndex() == 3);
// Test line boundary with meta modifier (from current position)
// First reset cursor to start
textInput->rawTextInput()->cursor(Cursor::zero());
handled = textInput->keyInput(Key::right, KeyModifiers::meta, true, false);
CHECK(handled == true);
// Should jump to end of line
CHECK(textInput->rawTextInput()->cursor().start().codePointIndex() == 13);
// Test sub-word boundary with alt+ctrl modifier
textInput->rawTextInput()->text("oneTwo threeF");
textInput->rawTextInput()->cursor(Cursor::zero());
artboard->advance(0.0f);
handled = textInput->keyInput(Key::right,
KeyModifiers::alt | KeyModifiers::ctrl,
true,
false);
CHECK(handled == true);
// Should jump to sub-word boundary (camelCase)
CHECK(textInput->rawTextInput()->cursor().start().codePointIndex() == 3);
}
TEST_CASE("text input keyInput handles shift for selection", "[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard != nullptr);
auto textInput = artboard->objects<TextInput>().first();
CHECK(textInput != nullptr);
textInput->rawTextInput()->text("hello world");
textInput->rawTextInput()->cursor(Cursor::zero());
artboard->advance(0.0f);
// Move right with shift should select
bool handled =
textInput->keyInput(Key::right, KeyModifiers::shift, true, false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->cursor().start().codePointIndex() == 0);
CHECK(textInput->rawTextInput()->cursor().end().codePointIndex() == 1);
// Continue selecting
handled = textInput->keyInput(Key::right, KeyModifiers::shift, true, false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->cursor().start().codePointIndex() == 0);
CHECK(textInput->rawTextInput()->cursor().end().codePointIndex() == 2);
}
TEST_CASE("text input textInput method inserts text", "[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard != nullptr);
auto textInput = artboard->objects<TextInput>().first();
CHECK(textInput != nullptr);
textInput->rawTextInput()->text("");
textInput->rawTextInput()->cursor(Cursor::zero());
artboard->advance(0.0f);
// Test textInput method
bool handled = textInput->textInput("hello");
CHECK(handled == true);
CHECK(textInput->rawTextInput()->text() == "hello");
// Insert more text
handled = textInput->textInput(" world");
CHECK(handled == true);
CHECK(textInput->rawTextInput()->text() == "hello world");
}
TEST_CASE("text input selectionRadiusChanged updates raw text input",
"[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard != nullptr);
auto textInput = artboard->objects<TextInput>().first();
CHECK(textInput != nullptr);
// Set selection radius
textInput->selectionRadius(5.0f);
// The selectionRadiusChanged callback should be invoked
// Just verify it doesn't crash and the value is set
CHECK(textInput->selectionRadius() == 5.0f);
}
TEST_CASE("state machine keyInput and textInput forward to text input",
"[text_input]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
CHECK(artboard != nullptr);
auto stateMachine = artboard->stateMachine(0);
if (stateMachine == nullptr)
{
// Skip if no state machine
return;
}
auto abi = artboard->instance();
StateMachineInstance smi(stateMachine, abi.get());
// Advance to initialize
smi.advance(0.0f);
auto textInput = abi->objects<TextInput>().first();
if (textInput == nullptr)
{
// Skip if no text input found
return;
}
// Clear text first
textInput->rawTextInput()->text("");
textInput->rawTextInput()->cursor(Cursor::zero());
// Test textInput through state machine
bool handled = smi.textInput("typed text");
CHECK(handled == true);
CHECK(textInput->rawTextInput()->text() == "typed text");
// Test keyInput through state machine (backspace)
handled = smi.keyInput(Key::backspace, KeyModifiers::none, true, false);
CHECK(handled == true);
CHECK(textInput->rawTextInput()->text() == "typed tex");
}
#endif