Skip to content

Commit f7745ed

Browse files
committed
Fix text input target
1 parent 719feb6 commit f7745ed

File tree

9 files changed

+280
-5
lines changed

9 files changed

+280
-5
lines changed

modules/yup_gui/component/yup_ComponentNative.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,19 @@ class YUP_API ComponentNative : public ReferenceCountedObject
356356
*/
357357
virtual const RectangleList<float>& getRepaintAreas() const = 0;
358358

359+
//==============================================================================
360+
/** Starts text input for the specified component.
361+
362+
@param component The component to start text input for.
363+
*/
364+
virtual void startTextInput (Component& component) = 0;
365+
366+
/** Stops text input for the specified component.
367+
368+
@param component The component to stop text input for.
369+
*/
370+
virtual void stopTextInput (Component& component) = 0;
371+
359372
//==============================================================================
360373
/** Gets the DPI scale factor.
361374
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
==============================================================================
3+
4+
This file is part of the YUP library.
5+
Copyright (c) 2026 - kunitoki@gmail.com
6+
7+
YUP is an open source library subject to open-source licensing.
8+
9+
The code included in this file is provided under the terms of the ISC license
10+
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
11+
to use, copy, modify, and/or distribute this software for any purpose with or
12+
without fee is hereby granted provided that the above copyright notice and
13+
this permission notice appear in all copies.
14+
15+
YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
16+
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
17+
DISCLAIMED.
18+
19+
==============================================================================
20+
*/
21+
22+
namespace yup
23+
{
24+
25+
//==============================================================================
26+
27+
void TextInputTarget::requestTextInput()
28+
{
29+
if (textInputActive)
30+
return;
31+
32+
textInputActive = true;
33+
34+
if (auto* component = dynamic_cast<Component*> (this))
35+
{
36+
if (auto* nativeComponent = component->getNativeComponent())
37+
nativeComponent->startTextInput (*component);
38+
}
39+
}
40+
41+
void TextInputTarget::relinquishTextInput()
42+
{
43+
if (! textInputActive)
44+
return;
45+
46+
textInputActive = false;
47+
48+
if (auto* component = dynamic_cast<Component*> (this))
49+
{
50+
if (auto* nativeComponent = component->getNativeComponent())
51+
nativeComponent->stopTextInput (*component);
52+
}
53+
}
54+
55+
bool TextInputTarget::isTextInputActive() const noexcept
56+
{
57+
return textInputActive;
58+
}
59+
60+
} // namespace yup
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
==============================================================================
3+
4+
This file is part of the YUP library.
5+
Copyright (c) 2026 - kunitoki@gmail.com
6+
7+
YUP is an open source library subject to open-source licensing.
8+
9+
The code included in this file is provided under the terms of the ISC license
10+
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
11+
to use, copy, modify, and/or distribute this software for any purpose with or
12+
without fee is hereby granted provided that the above copyright notice and
13+
this permission notice appear in all copies.
14+
15+
YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
16+
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
17+
DISCLAIMED.
18+
19+
==============================================================================
20+
*/
21+
22+
namespace yup
23+
{
24+
25+
//==============================================================================
26+
/**
27+
An interface for components that need to accept text input.
28+
29+
Components that need to accept text input (such as TextEditor) should inherit
30+
from this class and implement the required methods. This enables features like
31+
on-screen keyboards on mobile devices and IME (Input Method Editor) support.
32+
33+
The text input system is automatically managed based on focus changes. When a
34+
component that implements TextInputTarget gains focus, text input will be started.
35+
When it loses focus, text input will be stopped (unless the newly focused component
36+
also implements TextInputTarget).
37+
38+
Example usage:
39+
@code
40+
class MyTextComponent : public Component,
41+
public TextInputTarget
42+
{
43+
public:
44+
MyTextComponent()
45+
{
46+
setWantsKeyboardFocus (true);
47+
}
48+
49+
void focusGained() override
50+
{
51+
Component::focusGained();
52+
requestTextInput();
53+
}
54+
55+
void focusLost() override
56+
{
57+
Component::focusLost();
58+
relinquishTextInput();
59+
}
60+
61+
Rectangle<float> getTextInputRect() const override
62+
{
63+
return getLocalBounds();
64+
}
65+
};
66+
@endcode
67+
68+
@see Component, TextEditor
69+
*/
70+
class TextInputTarget
71+
{
72+
public:
73+
/** Destructor. */
74+
virtual ~TextInputTarget() = default;
75+
76+
//==============================================================================
77+
/**
78+
Called to get the screen rectangle where text input is being edited.
79+
80+
This rectangle is used to position on-screen keyboards and IME windows
81+
to avoid obscuring the text being edited.
82+
83+
@return The rectangle in screen coordinates where text is being edited
84+
*/
85+
virtual Rectangle<float> getTextInputRect() const = 0;
86+
87+
//==============================================================================
88+
/**
89+
Requests that the system starts accepting text input for this target.
90+
91+
Call this method when your component wants to receive text input events,
92+
typically in focusGained(). The system will show on-screen keyboards on
93+
mobile devices and enable IME where appropriate.
94+
*/
95+
void requestTextInput();
96+
97+
/**
98+
Relinquishes text input, telling the system this target no longer needs text input.
99+
100+
Call this method when your component no longer needs text input,
101+
typically in focusLost(). This will hide on-screen keyboards and
102+
disable IME.
103+
*/
104+
void relinquishTextInput();
105+
106+
/**
107+
Returns true if this target currently has active text input.
108+
109+
@return True if text input is currently active for this target
110+
*/
111+
bool isTextInputActive() const noexcept;
112+
113+
private:
114+
bool textInputActive = false;
115+
};
116+
117+
} // namespace yup

modules/yup_gui/native/yup_Windowing_sdl2.cpp

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ void SDL2ComponentNative::setFocusedComponent (Component* comp)
372372
{
373373
auto compBailOut = Component::BailOutChecker (comp);
374374

375+
// Check if we need to stop text input for the previously focused component
376+
Component* previousComponent = lastComponentFocused.get();
377+
bool previousWantsTextInput = previousComponent != nullptr && dynamic_cast<TextInputTarget*> (previousComponent) != nullptr;
378+
375379
if (lastComponentFocused != nullptr)
376380
{
377381
auto focusBailOut = Component::BailOutChecker (lastComponentFocused.get());
@@ -387,6 +391,14 @@ void SDL2ComponentNative::setFocusedComponent (Component* comp)
387391

388392
lastComponentFocused = comp;
389393

394+
// Check if the newly focused component needs text input
395+
Component* newComponent = lastComponentFocused.get();
396+
bool newWantsTextInput = newComponent != nullptr && dynamic_cast<TextInputTarget*> (newComponent) != nullptr;
397+
398+
// Stop text input if the previous component had it but the new one doesn't
399+
if (previousWantsTextInput && ! newWantsTextInput && previousComponent != nullptr)
400+
stopTextInput (*previousComponent);
401+
390402
if (lastComponentFocused != nullptr)
391403
{
392404
auto focusBailOut = Component::BailOutChecker (lastComponentFocused.get());
@@ -511,6 +523,46 @@ void* SDL2ComponentNative::getNativeHandle() const
511523

512524
//==============================================================================
513525

526+
void SDL2ComponentNative::startTextInput (Component& component)
527+
{
528+
if (window == nullptr)
529+
return;
530+
531+
auto* target = dynamic_cast<TextInputTarget*> (std::addressof (component));
532+
if (target == nullptr)
533+
return;
534+
535+
if (currentTextInputComponent != nullptr && currentTextInputComponent != std::addressof (component))
536+
SDL_StopTextInput();
537+
538+
currentTextInputComponent = std::addressof (component);
539+
540+
SDL_StartTextInput();
541+
542+
SDL_Rect sdlRect;
543+
auto textRect = target->getTextInputRect();
544+
sdlRect.x = static_cast<int> (textRect.getX());
545+
sdlRect.y = static_cast<int> (textRect.getY());
546+
sdlRect.w = static_cast<int> (textRect.getWidth());
547+
sdlRect.h = static_cast<int> (textRect.getHeight());
548+
SDL_SetTextInputRect (&sdlRect);
549+
}
550+
551+
void SDL2ComponentNative::stopTextInput (Component& component)
552+
{
553+
if (window == nullptr)
554+
return;
555+
556+
if (currentTextInputComponent == std::addressof (component))
557+
{
558+
currentTextInputComponent = nullptr;
559+
560+
SDL_StopTextInput();
561+
}
562+
}
563+
564+
//==============================================================================
565+
514566
void SDL2ComponentNative::run()
515567
{
516568
const double maxFrameTimeSeconds = 1.0 / static_cast<double> (desiredFrameRate);
@@ -990,23 +1042,28 @@ void SDL2ComponentNative::handleFocusChanged (bool gotFocus)
9901042

9911043
if (gotFocus)
9921044
{
993-
SDL_StartTextInput();
994-
9951045
if (! isRendering())
9961046
startRendering();
9971047

9981048
component.internalFocusChanged (true);
1049+
1050+
if (auto* comp = currentTextInputComponent.get())
1051+
{
1052+
if (auto* target = dynamic_cast<TextInputTarget*> (comp))
1053+
startTextInput (*comp);
1054+
}
9991055
}
10001056
else
10011057
{
1058+
if (currentTextInputComponent != nullptr)
1059+
SDL_StopTextInput();
1060+
10021061
component.internalFocusChanged (false);
10031062

10041063
lastComponentClicked = nullptr;
10051064
lastMouseDownPosition.reset();
10061065
lastMouseDownTime.reset();
10071066

1008-
SDL_StopTextInput();
1009-
10101067
if (updateOnlyWhenFocused)
10111068
{
10121069
if (isRendering())

modules/yup_gui/native/yup_Windowing_sdl2.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ class SDL2ComponentNative final
107107
//==============================================================================
108108
void* getNativeHandle() const override;
109109

110+
//==============================================================================
111+
void startTextInput (Component& component) override;
112+
void stopTextInput (Component& component) override;
113+
110114
//==============================================================================
111115
void run() override;
112116
void handleAsyncUpdate() override;
@@ -203,6 +207,8 @@ class SDL2ComponentNative final
203207
bool renderAtomicMode = false;
204208
bool renderWireframe = false;
205209
bool updateOnlyWhenFocused = false;
210+
211+
WeakReference<Component> currentTextInputComponent;
206212
};
207213

208214
} // namespace yup

modules/yup_gui/widgets/yup_TextEditor.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,14 @@ void TextEditor::resized()
315315
void TextEditor::focusGained()
316316
{
317317
startCaretBlinking();
318+
requestTextInput();
318319
repaint();
319320
}
320321

321322
void TextEditor::focusLost()
322323
{
323324
stopCaretBlinking();
325+
relinquishTextInput();
324326
repaint();
325327
}
326328

@@ -781,6 +783,16 @@ void TextEditor::moveCaretToEnd (bool extendSelection)
781783

782784
//==============================================================================
783785

786+
Rectangle<float> TextEditor::getTextInputRect() const
787+
{
788+
auto caretBounds = getCaretBounds();
789+
auto screenPos = localToScreen (caretBounds.getTopLeft());
790+
791+
return Rectangle<float> (screenPos.getX(), screenPos.getY(), caretBounds.getWidth(), caretBounds.getHeight());
792+
}
793+
794+
//==============================================================================
795+
784796
void TextEditor::handleBackspace()
785797
{
786798
if (readOnly)

modules/yup_gui/widgets/yup_TextEditor.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ namespace yup
4141
addAndMakeVisible(*editor);
4242
@endcode
4343
44-
@see Component, StyledText
44+
@see Component, StyledText, TextInputTarget
4545
*/
4646
class YUP_API TextEditor : public Component
47+
, public TextInputTarget
4748
{
4849
public:
4950
//==============================================================================
@@ -290,6 +291,13 @@ class YUP_API TextEditor : public Component
290291
/** @internal */
291292
StyledText& getStyledText() const noexcept { return const_cast<StyledText&> (styledText); }
292293

294+
//==============================================================================
295+
/** @internal
296+
Returns the rectangle where text input is being edited.
297+
This is used to position on-screen keyboards appropriately.
298+
*/
299+
Rectangle<float> getTextInputRect() const override;
300+
293301
private:
294302
//==============================================================================
295303
void updateStyledTextIfNeeded();

modules/yup_gui/yup_gui.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127

128128
#include "application/yup_Application.cpp"
129129
#include "desktop/yup_Desktop.cpp"
130+
#include "keyboard/yup_TextInputTarget.cpp"
130131
#include "mouse/yup_MouseEvent.cpp"
131132
#include "mouse/yup_MouseCursor.cpp"
132133
#include "clipboard/yup_SystemClipboard.cpp"

modules/yup_gui/yup_gui.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
#include "application/yup_Application.h"
7272
#include "keyboard/yup_KeyModifiers.h"
7373
#include "keyboard/yup_KeyPress.h"
74+
#include "keyboard/yup_TextInputTarget.h"
7475
#include "mouse/yup_MouseEvent.h"
7576
#include "mouse/yup_MouseCursor.h"
7677
#include "mouse/yup_MouseWheelData.h"

0 commit comments

Comments
 (0)