mirror of
https://github.com/zhenyan121/Cubed.git
synced 2026-08-08 17:57:02 +08:00
feat(ui): add TextField widget with UTF-8 text input support
This commit is contained in:
BIN
assets/texture/ui/textfield001.png
Normal file
BIN
assets/texture/ui/textfield001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 B |
@@ -36,5 +36,8 @@ protected:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
virtual bool handle_key_event(const KeyEvent&) { return false; }
|
virtual bool handle_key_event(const KeyEvent&) { return false; }
|
||||||
|
virtual bool handle_text_input_event(const TextInputEvent&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} // namespace Cubed
|
} // namespace Cubed
|
||||||
41
include/Cubed/tools/text_tools.hpp
Normal file
41
include/Cubed/tools/text_tools.hpp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
namespace Cubed {
|
||||||
|
inline std::string codepoint_to_utf8(uint32_t cp) {
|
||||||
|
std::string out;
|
||||||
|
|
||||||
|
if (cp <= 0x7F) {
|
||||||
|
out.push_back(static_cast<char>(cp));
|
||||||
|
} else if (cp <= 0x7FF) {
|
||||||
|
out.push_back(static_cast<char>(0xC0 | (cp >> 6)));
|
||||||
|
out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
|
||||||
|
} else if (cp <= 0xFFFF) {
|
||||||
|
out.push_back(static_cast<char>(0xE0 | (cp >> 12)));
|
||||||
|
out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
|
||||||
|
out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
|
||||||
|
} else {
|
||||||
|
out.push_back(static_cast<char>(0xF0 | (cp >> 18)));
|
||||||
|
out.push_back(static_cast<char>(0x80 | ((cp >> 12) & 0x3F)));
|
||||||
|
out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
|
||||||
|
out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void utf8_pop_back(std::string& str) {
|
||||||
|
if (str.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t i = str.size() - 1;
|
||||||
|
|
||||||
|
while (i > 0 && (static_cast<unsigned char>(str[i]) & 0xC0) == 0x80) {
|
||||||
|
--i;
|
||||||
|
}
|
||||||
|
|
||||||
|
str.erase(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Cubed
|
||||||
65
include/Cubed/ui/text_field.hpp
Normal file
65
include/Cubed/ui/text_field.hpp
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Cubed/ui/image.hpp"
|
||||||
|
#include "Cubed/ui/label.hpp"
|
||||||
|
#include "Cubed/ui/widget.hpp"
|
||||||
|
namespace Cubed {
|
||||||
|
class TextField : public Widget {
|
||||||
|
public:
|
||||||
|
TextField(const TextField&) = delete;
|
||||||
|
TextField(TextField&&) = delete;
|
||||||
|
TextField& operator=(const TextField&) = delete;
|
||||||
|
TextField& operator=(TextField&&) = delete;
|
||||||
|
TextField(Widget* parent);
|
||||||
|
|
||||||
|
TextField& set_scale(float scale);
|
||||||
|
|
||||||
|
float width() const override;
|
||||||
|
float height() const override;
|
||||||
|
|
||||||
|
TextField& set_width(float width);
|
||||||
|
TextField& set_height(float height);
|
||||||
|
TextField& set_show_text(const std::string& text);
|
||||||
|
TextField& set_background_image(const std::string& path,
|
||||||
|
TextureManager& texture_manager);
|
||||||
|
TextField& set_default_image(TextureManager& texture_manager);
|
||||||
|
TextField& set_auto_scale(bool auto_scale);
|
||||||
|
bool handle_mouse_move_event(const MouseMoveEvent& e) override;
|
||||||
|
bool handle_mouse_button_event(const MouseButtonEvent& e) override;
|
||||||
|
bool handle_text_input_event(const TextInputEvent& e) override;
|
||||||
|
bool handle_key_event(const KeyEvent& e) override;
|
||||||
|
|
||||||
|
const std::string& input_text() const;
|
||||||
|
|
||||||
|
template <typename F> TextField& set_on_finish(F&& f) {
|
||||||
|
m_on_finished = std::forward<F>(f);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr float PADDING = 5.0f;
|
||||||
|
static constexpr float DEFAULT_SCALE = 3.0f;
|
||||||
|
static constexpr float TEXT_SCALE = 0.6f;
|
||||||
|
|
||||||
|
static constexpr const char* DEFAULT_TEXT_FIELD_IMAGE =
|
||||||
|
"texture/ui/textfield001.png";
|
||||||
|
std::unique_ptr<Image> m_background;
|
||||||
|
std::unique_ptr<Label> m_foreground;
|
||||||
|
bool m_inside = false;
|
||||||
|
bool m_typing = false;
|
||||||
|
bool m_auto_scale = false;
|
||||||
|
|
||||||
|
float m_width = NORMAL_TEXTFIELD_WIDTH;
|
||||||
|
float m_height = NORMAL_TEXTFIELD_HEIGHT;
|
||||||
|
float m_scale = DEFAULT_SCALE;
|
||||||
|
|
||||||
|
std::string m_input_text;
|
||||||
|
std::string m_show_text;
|
||||||
|
|
||||||
|
std::function<void()> m_on_finished;
|
||||||
|
void update_text_scale();
|
||||||
|
void on_render(Renderer& renderer) override;
|
||||||
|
void on_update(float dt) override;
|
||||||
|
void update_show_text();
|
||||||
|
};
|
||||||
|
} // namespace Cubed
|
||||||
@@ -26,6 +26,7 @@ public:
|
|||||||
virtual bool handle_window_resize_event(const WindowResizeEvent& e);
|
virtual bool handle_window_resize_event(const WindowResizeEvent& e);
|
||||||
virtual bool handle_mouse_wheel_event(const MouseWheelEvent& e);
|
virtual bool handle_mouse_wheel_event(const MouseWheelEvent& e);
|
||||||
virtual bool handle_key_event(const KeyEvent& e);
|
virtual bool handle_key_event(const KeyEvent& e);
|
||||||
|
virtual bool handle_text_input_event(const TextInputEvent& e);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::unique_ptr<Widget> m_root_widget;
|
std::unique_ptr<Widget> m_root_widget;
|
||||||
|
|||||||
@@ -12,10 +12,16 @@ constexpr float NORMAL_BUTTON_WIDTH = 225.0f;
|
|||||||
constexpr float NORMAL_BUTTON_HEIGHT = 20.0f;
|
constexpr float NORMAL_BUTTON_HEIGHT = 20.0f;
|
||||||
constexpr float NORMAL_SLIDER_WIDTH = 225.0f;
|
constexpr float NORMAL_SLIDER_WIDTH = 225.0f;
|
||||||
constexpr float NORMAL_SLIDER_HEIGHT = 20.0f;
|
constexpr float NORMAL_SLIDER_HEIGHT = 20.0f;
|
||||||
|
constexpr float NORMAL_TEXTFIELD_WIDTH = 225.0f;
|
||||||
|
constexpr float NORMAL_TEXTFIELD_HEIGHT = 20.0f;
|
||||||
class Widget {
|
class Widget {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
Widget(const Widget&) = delete;
|
||||||
|
Widget(Widget&&) = delete;
|
||||||
|
Widget& operator=(const Widget&) = delete;
|
||||||
|
Widget& operator=(Widget&&) = delete;
|
||||||
|
|
||||||
Widget(const std::string& id, Widget* parent);
|
Widget(const std::string& id, Widget* parent);
|
||||||
Widget(Widget* parent);
|
Widget(Widget* parent);
|
||||||
virtual ~Widget() = default;
|
virtual ~Widget() = default;
|
||||||
@@ -35,7 +41,7 @@ public:
|
|||||||
virtual bool handle_mouse_wheel_event(const MouseWheelEvent& e);
|
virtual bool handle_mouse_wheel_event(const MouseWheelEvent& e);
|
||||||
virtual bool handle_window_resize_event(const WindowResizeEvent& e);
|
virtual bool handle_window_resize_event(const WindowResizeEvent& e);
|
||||||
virtual bool handle_mouse_move_event(const MouseMoveEvent& e);
|
virtual bool handle_mouse_move_event(const MouseMoveEvent& e);
|
||||||
|
virtual bool handle_text_input_event(const TextInputEvent& e);
|
||||||
template <typename T, typename... Args> T& add_child(Args&&... args) {
|
template <typename T, typename... Args> T& add_child(Args&&... args) {
|
||||||
auto widget = std::make_unique<T>(std::forward<Args>(args)..., this);
|
auto widget = std::make_unique<T>(std::forward<Args>(args)..., this);
|
||||||
T& ref = *widget;
|
T& ref = *widget;
|
||||||
|
|||||||
@@ -77,4 +77,5 @@ target_sources(${PROJECT_NAME}
|
|||||||
ui/settings_ui.cpp
|
ui/settings_ui.cpp
|
||||||
scene/settings_scene.cpp
|
scene/settings_scene.cpp
|
||||||
ui/combo_button.cpp
|
ui/combo_button.cpp
|
||||||
|
ui/text_field.cpp
|
||||||
)
|
)
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "Cubed/tools/cubed_assert.hpp"
|
#include "Cubed/tools/cubed_assert.hpp"
|
||||||
#include "Cubed/tools/log.hpp"
|
#include "Cubed/tools/log.hpp"
|
||||||
#include "Cubed/tools/system_info.hpp"
|
#include "Cubed/tools/system_info.hpp"
|
||||||
|
#include "Cubed/tools/text_tools.hpp"
|
||||||
#include "version.hpp"
|
#include "version.hpp"
|
||||||
|
|
||||||
#include <exception>
|
#include <exception>
|
||||||
@@ -638,6 +639,7 @@ void App::char_callback(GLFWwindow* window, unsigned int c) {
|
|||||||
ImGui_ImplGlfw_CharCallback(window, c);
|
ImGui_ImplGlfw_CharCallback(window, c);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
app->dispatch_event(TextInputEvent{codepoint_to_utf8(c)});
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::render() {
|
void App::render() {
|
||||||
|
|||||||
170
src/ui/text_field.cpp
Normal file
170
src/ui/text_field.cpp
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#include "Cubed/ui/text_field.hpp"
|
||||||
|
|
||||||
|
#include "Cubed/tools/text_tools.hpp"
|
||||||
|
|
||||||
|
namespace Cubed {
|
||||||
|
TextField::TextField(Widget* parent) : Widget(parent) {
|
||||||
|
|
||||||
|
m_background = std::make_unique<Image>(this);
|
||||||
|
m_background->set_fill(true);
|
||||||
|
m_background->set_anchor(Anchor::TOP_LEFT);
|
||||||
|
|
||||||
|
m_foreground = std::make_unique<Label>(this);
|
||||||
|
m_foreground->set_anchor(Anchor::CENTER_LEFT);
|
||||||
|
m_foreground->set_offset({10, 0});
|
||||||
|
update_text_scale();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextField::update_text_scale() {
|
||||||
|
if (!m_auto_scale) {
|
||||||
|
m_foreground->set_scale(TEXT_SCALE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float text_w = m_foreground->real_width();
|
||||||
|
float text_h = m_foreground->real_height();
|
||||||
|
|
||||||
|
if (text_w <= 0.0f || text_h <= 0.0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float available_w = std::max(0.0f, width() - 2.0f * PADDING * m_scale);
|
||||||
|
float available_h = std::max(0.0f, height() - 2.0f * PADDING * m_scale);
|
||||||
|
|
||||||
|
float scale_x = available_w / text_w;
|
||||||
|
float scale_y = available_h / text_h;
|
||||||
|
|
||||||
|
float scale = std::max(0.0f, std::min(scale_x, scale_y));
|
||||||
|
scale = std::clamp(scale, 0.01f, 100.0f);
|
||||||
|
m_foreground->set_scale(scale);
|
||||||
|
}
|
||||||
|
void TextField::on_render(Renderer& renderer) {
|
||||||
|
if (m_background) {
|
||||||
|
m_background->render(renderer);
|
||||||
|
}
|
||||||
|
if (m_foreground) {
|
||||||
|
m_foreground->render(renderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void TextField::on_update(float dt) {
|
||||||
|
if (m_background) {
|
||||||
|
m_background->update(dt);
|
||||||
|
}
|
||||||
|
if (m_foreground) {
|
||||||
|
m_foreground->update(dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextField::update_show_text() {
|
||||||
|
if (m_input_text.empty()) {
|
||||||
|
if (!m_typing) {
|
||||||
|
m_foreground->set_text(m_show_text);
|
||||||
|
m_foreground->set_color(Color::GRAY);
|
||||||
|
} else {
|
||||||
|
m_foreground->set_text(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
m_foreground->set_text(m_input_text);
|
||||||
|
m_foreground->set_color(Color::WHITE);
|
||||||
|
}
|
||||||
|
update_text_scale();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField& TextField::set_scale(float scale) {
|
||||||
|
m_scale = scale;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
float TextField::width() const { return m_width * m_scale; }
|
||||||
|
float TextField::height() const { return m_height * m_scale; }
|
||||||
|
|
||||||
|
TextField& TextField::set_width(float width) {
|
||||||
|
m_width = width;
|
||||||
|
update_text_scale();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
TextField& TextField::set_height(float height) {
|
||||||
|
m_height = height;
|
||||||
|
update_text_scale();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
TextField& TextField::set_show_text(const std::string& text) {
|
||||||
|
m_show_text = text;
|
||||||
|
update_show_text();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
TextField& TextField::set_background_image(const std::string& path,
|
||||||
|
TextureManager& texture_manager) {
|
||||||
|
m_background->set_image(path, texture_manager);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField& TextField::set_default_image(TextureManager& texture_manager) {
|
||||||
|
return set_background_image(DEFAULT_TEXT_FIELD_IMAGE, texture_manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField& TextField::set_auto_scale(bool auto_scale) {
|
||||||
|
m_auto_scale = auto_scale;
|
||||||
|
update_text_scale();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& TextField::input_text() const { return m_input_text; }
|
||||||
|
|
||||||
|
bool TextField::handle_mouse_move_event(const MouseMoveEvent& e) {
|
||||||
|
auto p = pos();
|
||||||
|
if (e.xpos >= p.x && e.xpos <= p.x + width() && e.ypos >= p.y &&
|
||||||
|
e.ypos <= p.y + height()) {
|
||||||
|
m_inside = true;
|
||||||
|
} else {
|
||||||
|
m_inside = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool TextField::handle_mouse_button_event(const MouseButtonEvent& e) {
|
||||||
|
if (e.key == MouseKey::LEFT_BUTTON && e.action == KeyAction::PRESS) {
|
||||||
|
if (m_inside) {
|
||||||
|
m_typing = true;
|
||||||
|
update_show_text();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (m_typing) {
|
||||||
|
m_typing = false;
|
||||||
|
if (m_on_finished) {
|
||||||
|
m_on_finished();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool TextField::handle_text_input_event(const TextInputEvent& e) {
|
||||||
|
if (m_typing) {
|
||||||
|
m_input_text.append(e.text);
|
||||||
|
update_show_text();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool TextField::handle_key_event(const KeyEvent& e) {
|
||||||
|
if (e.key == Key::ENTER && e.action == KeyAction::PRESS) {
|
||||||
|
if (m_typing) {
|
||||||
|
m_typing = false;
|
||||||
|
if (m_on_finished) {
|
||||||
|
m_on_finished();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.key == Key::BACKSPACE &&
|
||||||
|
(e.action == KeyAction::PRESS || e.action == KeyAction::REPEAT)) {
|
||||||
|
if (m_typing) {
|
||||||
|
utf8_pop_back(m_input_text);
|
||||||
|
update_show_text();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} // namespace Cubed
|
||||||
@@ -53,7 +53,12 @@ bool UIManager::handle_event(const Event& e) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[](const TextInputEvent&) { return false; },
|
[this](const TextInputEvent& e) {
|
||||||
|
if (handle_text_input_event(e)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
[this](const WindowResizeEvent& e) {
|
[this](const WindowResizeEvent& e) {
|
||||||
handle_window_resize_event(e);
|
handle_window_resize_event(e);
|
||||||
return false;
|
return false;
|
||||||
@@ -101,4 +106,12 @@ bool UIManager::handle_key_event(const KeyEvent& e) {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UIManager::handle_text_input_event(const TextInputEvent& e) {
|
||||||
|
if (m_root_widget->handle_text_input_event(e)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Cubed
|
} // namespace Cubed
|
||||||
@@ -155,6 +155,7 @@ bool Widget::handle_window_resize_event(const WindowResizeEvent& e) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Widget::handle_mouse_move_event(const MouseMoveEvent& e) {
|
bool Widget::handle_mouse_move_event(const MouseMoveEvent& e) {
|
||||||
for (auto it = m_children.rbegin(); it != m_children.rend(); ++it) {
|
for (auto it = m_children.rbegin(); it != m_children.rend(); ++it) {
|
||||||
if ((*it)->handle_mouse_move_event(e)) {
|
if ((*it)->handle_mouse_move_event(e)) {
|
||||||
@@ -163,4 +164,14 @@ bool Widget::handle_mouse_move_event(const MouseMoveEvent& e) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Widget::handle_text_input_event(const TextInputEvent& e) {
|
||||||
|
for (auto it = m_children.rbegin(); it != m_children.rend(); ++it) {
|
||||||
|
if ((*it)->handle_text_input_event(e)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Cubed
|
} // namespace Cubed
|
||||||
Reference in New Issue
Block a user