mirror of
https://github.com/rive-app/rive-cpp.git
synced 2026-01-18 21:21:17 +01:00
feat: Text input from editor + start of Focusable (#9905) e5c8a7ae8f
* feat: working on focusable * feat: text events to rive native * feat: runtime text from editor * feat: text input in the editor * chore: cleanup * chore: fix non text builds * chore: fix warning * fix: key import in rive_native_web * feat: text input for web rive_native * chore: fixes * chore: cleanup * fix: remove unused imports * chore: more tests for missed lines Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
#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"
|
||||
@@ -42,4 +44,296 @@ TEST_CASE("file with text input renders correctly", "[silver]")
|
||||
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
|
||||
Reference in New Issue
Block a user