Use SDL text input correctly, add basic composition support

This commit is contained in:
Tamás Bálint Misius 2021-03-12 21:02:02 +01:00
parent da2ccc70fe
commit 3dbf6d7810
No known key found for this signature in database
GPG Key ID: 5B472A12F6ECA9F2
12 changed files with 291 additions and 57 deletions

View File

@ -7,3 +7,6 @@ void ClipboardPush(ByteString text);
ByteString ClipboardPull(); ByteString ClipboardPull();
int GetModifiers(); int GetModifiers();
unsigned int GetTicks(); unsigned int GetTicks();
void StartTextInput();
void StopTextInput();
void SetTextInputRect(int x, int y, int w, int h);

View File

@ -60,6 +60,26 @@ bool forceIntegerScaling = true;
bool resizable = false; bool resizable = false;
void StartTextInput()
{
SDL_StartTextInput();
}
void StopTextInput()
{
SDL_StopTextInput();
}
void SetTextInputRect(int x, int y, int w, int h)
{
SDL_Rect rect;
rect.x = x;
rect.y = y;
rect.w = w;
rect.h = h;
SDL_SetTextInputRect(&rect);
}
void ClipboardPush(ByteString text) void ClipboardPush(ByteString text)
{ {
SDL_SetClipboardText(text.c_str()); SDL_SetClipboardText(text.c_str());
@ -267,6 +287,13 @@ void EventProcess(SDL_Event event)
} }
engine->onTextInput(ByteString(event.text.text).FromUtf8()); engine->onTextInput(ByteString(event.text.text).FromUtf8());
break; break;
case SDL_TEXTEDITING:
if (SDL_GetModState() & KMOD_GUI)
{
break;
}
engine->onTextEditing(ByteString(event.edit.text).FromUtf8(), event.edit.start);
break;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
{ {
int x = event.wheel.x; int x = event.wheel.x;

View File

@ -69,6 +69,25 @@ bool forceIntegerScaling = true;
bool resizable = false; bool resizable = false;
bool momentumScroll = true; bool momentumScroll = true;
void StartTextInput()
{
SDL_StartTextInput();
}
void StopTextInput()
{
SDL_StopTextInput();
}
void SetTextInputRect(int x, int y, int w, int h)
{
SDL_Rect rect;
rect.x = x;
rect.y = y;
rect.w = w;
rect.h = h;
SDL_SetTextInputRect(&rect);
}
void ClipboardPush(ByteString text) void ClipboardPush(ByteString text)
{ {
@ -425,6 +444,13 @@ void EventProcess(SDL_Event event)
} }
engine->onTextInput(ByteString(event.text.text).FromUtf8()); engine->onTextInput(ByteString(event.text.text).FromUtf8());
break; break;
case SDL_TEXTEDITING:
if (SDL_GetModState() & KMOD_GUI)
{
break;
}
engine->onTextEditing(ByteString(event.edit.text).FromUtf8(), event.edit.start);
break;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
{ {
int x = event.wheel.x; int x = event.wheel.x;

View File

@ -1282,8 +1282,6 @@ void GameView::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl,
switch(scan) switch(scan)
{ {
case SDL_SCANCODE_GRAVE: case SDL_SCANCODE_GRAVE:
SDL_StopTextInput();
SDL_StartTextInput();
c->ShowConsole(); c->ShowConsole();
break; break;
case SDL_SCANCODE_SPACE: //Space case SDL_SCANCODE_SPACE: //Space

View File

@ -19,7 +19,8 @@ Component::Component(Window* parent_state):
Position(Point(0,0)), Position(Point(0,0)),
Size(Point(0,0)), Size(Point(0,0)),
Enabled(true), Enabled(true),
Visible(true) Visible(true),
DoesTextInput(false)
{ {
} }
@ -35,7 +36,8 @@ Component::Component(Point position, Point size):
Position(position), Position(position),
Size(size), Size(size),
Enabled(true), Enabled(true),
Visible(true) Visible(true),
DoesTextInput(false)
{ {
} }
@ -51,7 +53,8 @@ Component::Component():
Position(Point(0,0)), Position(Point(0,0)),
Size(Point(0,0)), Size(Point(0,0)),
Enabled(true), Enabled(true),
Visible(true) Visible(true),
DoesTextInput(false)
{ {
} }
@ -201,6 +204,10 @@ void Component::OnTextInput(String text)
{ {
} }
void Component::OnTextEditing(String text)
{
}
void Component::OnMouseClick(int localx, int localy, unsigned button) void Component::OnMouseClick(int localx, int localy, unsigned button)
{ {
} }

View File

@ -29,6 +29,7 @@ namespace ui
ui::Point iconPosition; ui::Point iconPosition;
ui::ContextMenu * menu; ui::ContextMenu * menu;
Graphics * GetGraphics(); Graphics * GetGraphics();
public: public:
Component(Window* parent_state); Component(Window* parent_state);
Component(Point position, Point size); Component(Point position, Point size);
@ -45,6 +46,7 @@ namespace ui
Point Size; Point Size;
bool Enabled; bool Enabled;
bool Visible; bool Visible;
bool DoesTextInput;
ui::Appearance Appearance; ui::Appearance Appearance;
//virtual void SetAppearance(ui::Appearance); //virtual void SetAppearance(ui::Appearance);
@ -204,5 +206,6 @@ namespace ui
virtual void OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt); virtual void OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt);
virtual void OnTextInput(String text); virtual void OnTextInput(String text);
virtual void OnTextEditing(String text);
}; };
} }

View File

@ -260,8 +260,38 @@ void Engine::onKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl,
void Engine::onTextInput(String text) void Engine::onTextInput(String text)
{ {
if (textInput)
{
if (state_ && !ignoreEvents) if (state_ && !ignoreEvents)
state_->DoTextInput(text); state_->DoTextInput(text);
}
}
void Engine::onTextEditing(String text, int start)
{
if (textInput)
{
// * SDL sends the candidate string in packets of some arbitrary size,
// leaving it up to the user to assemble these packets into the
// complete candidate string. The start parameter tells us which
// portion of the candidate string the current packet spans.
// * Sadly, there's no documented way to tell the first or last packet
// apart from the rest. While there's also no documented guarantee
// that the packets come in order and that there are no gaps or
// overlaps between them, the implementation on the SDL side seems to
// ensure this. So what we do is just append whatever packet we get
// to a buffer, which we reset every time a "first-y looking" packet
// arrives. We also forward a textediting event on every packet,
// which is redundant, but should be okay, as textediting events are
// not supposed to have an effect on the actual text being edited.
if (start == 0)
{
textEditingBuf.clear();
}
textEditingBuf.append(text);
if (state_ && !ignoreEvents)
state_->DoTextEditing(textEditingBuf);
}
} }
void Engine::onMouseClick(int x, int y, unsigned button) void Engine::onMouseClick(int x, int y, unsigned button)
@ -318,3 +348,28 @@ void Engine::onFileDrop(ByteString filename)
if (state_) if (state_)
state_->DoFileDrop(filename); state_->DoFileDrop(filename);
} }
void Engine::StartTextInput()
{
if (textInput)
{
return;
}
textInput = true;
::StartTextInput();
}
void Engine::StopTextInput()
{
if (!textInput)
{
return;
}
::StopTextInput();
textInput = false;
}
void Engine::TextInputRect(Point position, Point size)
{
::SetTextInputRect(position.X * Scale, position.Y * Scale, size.X * Scale, size.Y * Scale);
}

View File

@ -33,6 +33,7 @@ namespace ui
void onKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt); void onKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt);
void onKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt); void onKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt);
void onTextInput(String text); void onTextInput(String text);
void onTextEditing(String text, int start);
void onResize(int newWidth, int newHeight); void onResize(int newWidth, int newHeight);
void onClose(); void onClose();
void onFileDrop(ByteString filename); void onFileDrop(ByteString filename);
@ -80,6 +81,10 @@ namespace ui
inline void SetSize(int width, int height); inline void SetSize(int width, int height);
void StartTextInput();
void StopTextInput();
void TextInputRect(Point position, Point size);
//void SetState(Window* state); //void SetState(Window* state);
//inline State* GetState() { return state_; } //inline State* GetState() { return state_; }
inline Window* GetWindow() { return state_; } inline Window* GetWindow() { return state_; }
@ -95,6 +100,8 @@ namespace ui
bool forceIntegerScaling = true; bool forceIntegerScaling = true;
bool resizable; bool resizable;
bool textInput = false;
float dt; float dt;
float fps; float fps;
pixel * lastBuffer; pixel * lastBuffer;
@ -125,6 +132,8 @@ namespace ui
bool momentumScroll; bool momentumScroll;
String textEditingBuf;
public: public:
inline void SetMomentumScroll(bool newMomentumScroll) inline void SetMomentumScroll(bool newMomentumScroll)
{ {

View File

@ -6,10 +6,12 @@
#include "PowderToy.h" #include "PowderToy.h"
#include "graphics/Graphics.h" #include "graphics/Graphics.h"
#include "graphics/FontReader.h"
#include "gui/interface/Point.h" #include "gui/interface/Point.h"
#include "gui/interface/Keys.h" #include "gui/interface/Keys.h"
#include "gui/interface/Mouse.h" #include "gui/interface/Mouse.h"
#include "gui/interface/Engine.h"
#include "ContextMenu.h" #include "ContextMenu.h"
@ -24,8 +26,11 @@ Textbox::Textbox(Point position, Point size, String textboxText, String textboxP
characterDown(0), characterDown(0),
mouseDown(false), mouseDown(false),
masked(false), masked(false),
border(true) border(true),
inputRectPosition(0, 0),
textEditing(false)
{ {
DoesTextInput = true;
placeHolder = textboxPlaceholder; placeHolder = textboxPlaceholder;
SetText(textboxText); SetText(textboxText);
@ -56,6 +61,8 @@ void Textbox::SetPlaceholder(String text)
void Textbox::SetText(String newText) void Textbox::SetText(String newText)
{ {
StopTextEditing();
backingText = newText; backingText = newText;
if(masked) if(masked)
@ -69,14 +76,7 @@ void Textbox::SetText(String newText)
cursor = newText.length(); cursor = newText.length();
if(cursor) resetCursorPosition();
{
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
}
else
{
cursorPositionY = cursorPositionX = 0;
}
} }
Textbox::ValidInput Textbox::GetInputType() Textbox::ValidInput Textbox::GetInputType()
@ -106,6 +106,8 @@ String Textbox::GetText()
void Textbox::OnContextMenuAction(int item) void Textbox::OnContextMenuAction(int item)
{ {
StopTextEditing();
switch(item) switch(item)
{ {
case 0: case 0:
@ -122,7 +124,14 @@ void Textbox::OnContextMenuAction(int item)
void Textbox::resetCursorPosition() void Textbox::resetCursorPosition()
{ {
if(cursor)
{
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY); textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
}
else
{
cursorPositionY = cursorPositionX = 0;
}
} }
void Textbox::TabFocus() void Textbox::TabFocus()
@ -133,6 +142,8 @@ void Textbox::TabFocus()
void Textbox::cutSelection() void Textbox::cutSelection()
{ {
StopTextEditing();
if (HasSelection()) if (HasSelection())
{ {
if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length()) if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
@ -167,20 +178,15 @@ void Textbox::cutSelection()
updateSelection(); updateSelection();
TextPosition(displayTextWrapper.WrappedText()); TextPosition(displayTextWrapper.WrappedText());
if(cursor) resetCursorPosition();
{
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
}
else
{
cursorPositionY = cursorPositionX = 0;
}
if (actionCallback.change) if (actionCallback.change)
actionCallback.change(); actionCallback.change();
} }
void Textbox::pasteIntoSelection() void Textbox::pasteIntoSelection()
{ {
StopTextEditing();
String newText = format::CleanString(ClipboardPull().FromUtf8(), false, true, inputType != Multiline, inputType == Number || inputType == Numeric); String newText = format::CleanString(ClipboardPull().FromUtf8(), false, true, inputType != Multiline, inputType == Number || inputType == Numeric);
if (HasSelection()) if (HasSelection())
{ {
@ -237,14 +243,7 @@ void Textbox::pasteIntoSelection()
updateSelection(); updateSelection();
TextPosition(displayTextWrapper.WrappedText()); TextPosition(displayTextWrapper.WrappedText());
if(cursor) resetCursorPosition();
{
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
}
else
{
cursorPositionY = cursorPositionX = 0;
}
if (actionCallback.change) if (actionCallback.change)
actionCallback.change(); actionCallback.change();
} }
@ -280,6 +279,10 @@ bool Textbox::StringValid(String text)
void Textbox::Tick(float dt) void Textbox::Tick(float dt)
{ {
Label::Tick(dt); Label::Tick(dt);
if (GetParentWindow() && Visible && Enabled && IsFocused())
{
ui::Engine::Ref().TextInputRect(GetScreenPos() + textPosition + inputRectPosition - Point(1, 3), Point(Size.X - textPosition.X - inputRectPosition.X, FONT_H + 2));
}
if (!IsFocused()) if (!IsFocused())
{ {
keyDown = 0; keyDown = 0;
@ -336,19 +339,23 @@ void Textbox::OnVKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl,
switch(key) switch(key)
{ {
case SDLK_HOME: case SDLK_HOME:
StopTextEditing();
cursor = 0; cursor = 0;
ClearSelection(); ClearSelection();
break; break;
case SDLK_END: case SDLK_END:
StopTextEditing();
cursor = backingText.length(); cursor = backingText.length();
ClearSelection(); ClearSelection();
break; break;
case SDLK_LEFT: case SDLK_LEFT:
StopTextEditing();
if(cursor > 0) if(cursor > 0)
cursor--; cursor--;
ClearSelection(); ClearSelection();
break; break;
case SDLK_RIGHT: case SDLK_RIGHT:
StopTextEditing();
if (cursor < (int)backingText.length()) if (cursor < (int)backingText.length())
cursor++; cursor++;
ClearSelection(); ClearSelection();
@ -356,6 +363,7 @@ void Textbox::OnVKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl,
case SDLK_DELETE: case SDLK_DELETE:
if(ReadOnly) if(ReadOnly)
break; break;
StopTextEditing();
if (HasSelection()) if (HasSelection())
{ {
if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length()) if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
@ -382,6 +390,7 @@ void Textbox::OnVKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl,
case SDLK_BACKSPACE: case SDLK_BACKSPACE:
if (ReadOnly) if (ReadOnly)
break; break;
StopTextEditing();
if (HasSelection()) if (HasSelection())
{ {
if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length()) if (getLowerSelectionBound() < 0 || getHigherSelectionBound() > (int)backingText.length())
@ -455,19 +464,18 @@ void Textbox::AfterTextChange(bool changed)
updateSelection(); updateSelection();
TextPosition(displayTextWrapper.WrappedText()); TextPosition(displayTextWrapper.WrappedText());
if(cursor) resetCursorPosition();
{
textWrapper.Index2Point(textWrapper.Clear2Index(cursor), cursorPositionX, cursorPositionY);
}
else
{
cursorPositionY = cursorPositionX = 0;
}
if (changed && actionCallback.change) if (changed && actionCallback.change)
actionCallback.change(); actionCallback.change();
} }
void Textbox::OnTextInput(String text) void Textbox::OnTextInput(String text)
{
StopTextEditing();
InsertText(text);
}
void Textbox::InsertText(String text)
{ {
if (StringValid(text) && !ReadOnly) if (StringValid(text) && !ReadOnly)
{ {
@ -501,21 +509,80 @@ void Textbox::OnTextInput(String text)
} }
} }
void Textbox::StartTextEditing()
{
if (ReadOnly || textEditing)
{
return;
}
textEditing = true;
selectionIndexLSave1 = selectionIndexL.clear_index;
selectionIndexHSave1 = selectionIndexH.clear_index;
backingTextSave1 = backingText;
cursorSave1 = cursor;
InsertText(String(""));
selectionIndexLSave2 = selectionIndexL.clear_index;
selectionIndexHSave2 = selectionIndexH.clear_index;
backingTextSave2 = backingText;
cursorSave2 = cursor;
inputRectPosition.X = cursorPositionX;
inputRectPosition.Y = cursorPositionY;
}
void Textbox::StopTextEditing()
{
if (ReadOnly || !textEditing)
{
return;
}
textEditing = false;
backingText = backingTextSave1;
AfterTextChange(true);
selectionIndexL = textWrapper.Clear2Index(selectionIndexLSave1);
selectionIndexH = textWrapper.Clear2Index(selectionIndexHSave1);
selectionIndex0 = selectionIndexL;
selectionIndex1 = selectionIndexH;
cursor = cursorSave1;
updateSelection();
}
void Textbox::OnTextEditing(String text)
{
if (!StringValid(text) || ReadOnly)
{
return;
}
if (!text.size())
{
StopTextEditing();
return;
}
StartTextEditing();
backingText = backingTextSave2;
AfterTextChange(true);
selectionIndexL = textWrapper.Clear2Index(selectionIndexLSave2);
selectionIndexH = textWrapper.Clear2Index(selectionIndexHSave2);
selectionIndex0 = selectionIndexL;
selectionIndex1 = selectionIndexH;
cursor = cursorSave2;
updateSelection();
InsertText(text);
selectionIndex1 = textWrapper.Clear2Index(cursor);
selectionIndex0 = textWrapper.Clear2Index(cursor - int(text.size()));
selectionIndexL = selectionIndex0;
selectionIndexH = selectionIndex1;
updateSelection();
}
void Textbox::OnMouseClick(int x, int y, unsigned button) void Textbox::OnMouseClick(int x, int y, unsigned button)
{ {
if (button != SDL_BUTTON_RIGHT) if (button != SDL_BUTTON_RIGHT)
{ {
StopTextEditing();
mouseDown = true; mouseDown = true;
auto index = textWrapper.Point2Index(x-textPosition.X, y-textPosition.Y); auto index = textWrapper.Point2Index(x-textPosition.X, y-textPosition.Y);
cursor = index.raw_index; cursor = index.raw_index;
if(cursor) resetCursorPosition();
{
textWrapper.Index2Point(index, cursorPositionX, cursorPositionY);
}
else
{
cursorPositionY = cursorPositionX = 0;
}
} }
Label::OnMouseClick(x, y, button); Label::OnMouseClick(x, y, button);
} }
@ -532,14 +599,7 @@ void Textbox::OnMouseMoved(int localx, int localy, int dx, int dy)
{ {
auto index = textWrapper.Point2Index(localx-textPosition.X, localy-textPosition.Y); auto index = textWrapper.Point2Index(localx-textPosition.X, localy-textPosition.Y);
cursor = index.raw_index; cursor = index.raw_index;
if(cursor) resetCursorPosition();
{
textWrapper.Index2Point(index, cursorPositionX, cursorPositionY);
}
else
{
cursorPositionY = cursorPositionX = 0;
}
} }
Label::OnMouseMoved(localx, localy, dx, dy); Label::OnMouseMoved(localx, localy, dx, dy);
} }

View File

@ -15,6 +15,9 @@ struct TextboxAction
class Textbox : public Label class Textbox : public Label
{ {
void AfterTextChange(bool changed); void AfterTextChange(bool changed);
void InsertText(String text);
void StartTextEditing();
void StopTextEditing();
public: public:
bool ReadOnly; bool ReadOnly;
@ -53,6 +56,7 @@ public:
void OnVKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt); void OnVKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt);
void OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) override; void OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) override;
void OnTextInput(String text) override; void OnTextInput(String text) override;
void OnTextEditing(String text) override;
void Draw(const Point& screenPos) override; void Draw(const Point& screenPos) override;
protected: protected:
@ -68,6 +72,21 @@ protected:
String backingText; String backingText;
String placeHolder; String placeHolder;
// * Cursor state to reset to before inserting actual input in StopTextEditing.
int selectionIndexLSave1;
int selectionIndexHSave1;
String backingTextSave1;
int cursorSave1;
// * Cursor state to reset to before inserting a candidate string in OnTextEditing.
int selectionIndexLSave2;
int selectionIndexHSave2;
String backingTextSave2;
int cursorSave2;
Point inputRectPosition;
bool textEditing;
virtual void cutSelection(); virtual void cutSelection();
virtual void pasteIntoSelection(); virtual void pasteIntoSelection();
}; };

View File

@ -261,6 +261,16 @@ void Window::DoTick(float dt)
if (debugMode) if (debugMode)
return; return;
#endif #endif
if (focusedComponent_ && focusedComponent_->Visible && focusedComponent_->Enabled && focusedComponent_->DoesTextInput)
{
ui::Engine::Ref().StartTextInput();
}
else
{
ui::Engine::Ref().StopTextInput();
}
//on mouse hover //on mouse hover
for (int i = Components.size() - 1; i >= 0 && !halt; --i) for (int i = Components.size() - 1; i >= 0 && !halt; --i)
{ {
@ -435,6 +445,21 @@ void Window::DoTextInput(String text)
finalise(); finalise();
} }
void Window::DoTextEditing(String text)
{
if (focusedComponent_ != NULL)
{
if (focusedComponent_->Enabled && focusedComponent_->Visible)
focusedComponent_->OnTextEditing(text);
}
if (!stop)
OnTextEditing(text);
if (destruct)
finalise();
}
void Window::DoMouseDown(int x_, int y_, unsigned button) void Window::DoMouseDown(int x_, int y_, unsigned button)
{ {
//on mouse click //on mouse click

View File

@ -68,6 +68,7 @@ namespace ui
virtual void DoKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt); virtual void DoKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt);
virtual void DoKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt); virtual void DoKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt);
virtual void DoTextInput(String text); virtual void DoTextInput(String text);
virtual void DoTextEditing(String text);
// Sets halt and destroy, this causes the Windows to stop sending events and remove itself. // Sets halt and destroy, this causes the Windows to stop sending events and remove itself.
void SelfDestruct(); void SelfDestruct();
@ -107,6 +108,7 @@ namespace ui
virtual void OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) {} virtual void OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) {}
virtual void OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) {} virtual void OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) {}
virtual void OnTextInput(String text) {} virtual void OnTextInput(String text) {}
virtual void OnTextEditing(String text) {}
std::vector<Component*> Components; std::vector<Component*> Components;
Component *focusedComponent_; Component *focusedComponent_;
Component *hoverComponent; Component *hoverComponent;