From fbc91bfe8ce8f7cc176307fd6993ba9ee95e2071 Mon Sep 17 00:00:00 2001 From: apio Date: Fri, 29 Mar 2024 20:16:40 +0100 Subject: [PATCH] libui: Add a TextInput base class to handle most input fields and add an InputField class for single-line inputs --- libui/CMakeLists.txt | 2 + libui/include/ui/InputField.h | 40 ++++++++++++ libui/include/ui/TextInput.h | 41 +++++++++++++ libui/src/InputField.cpp | 112 ++++++++++++++++++++++++++++++++++ libui/src/TextInput.cpp | 52 ++++++++++++++++ 5 files changed, 247 insertions(+) create mode 100644 libui/include/ui/InputField.h create mode 100644 libui/include/ui/TextInput.h create mode 100644 libui/src/InputField.cpp create mode 100644 libui/src/TextInput.cpp diff --git a/libui/CMakeLists.txt b/libui/CMakeLists.txt index 04dcfcdc..fb325b54 100644 --- a/libui/CMakeLists.txt +++ b/libui/CMakeLists.txt @@ -17,6 +17,8 @@ set(SOURCES src/Container.cpp src/Button.cpp src/Label.cpp + src/InputField.cpp + src/TextInput.cpp ) add_library(ui ${SOURCES}) diff --git a/libui/include/ui/InputField.h b/libui/include/ui/InputField.h new file mode 100644 index 00000000..bd3bab56 --- /dev/null +++ b/libui/include/ui/InputField.h @@ -0,0 +1,40 @@ +/** + * @file InputField.h + * @author apio (cloudapio.eu) + * @brief Single line text input widget. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#pragma once +#include +#include +#include + +namespace ui +{ + class InputField : public ui::TextInput + { + public: + InputField(SharedPtr font); + + Result handle_key_event(const ui::KeyEventRequest& request) override; + + Result draw(ui::Canvas& canvas) override; + + StringView data(); + + void on_submit(os::Function&& action) + { + m_on_submit_action = move(action); + m_has_on_submit_action = true; + } + + private: + SharedPtr m_font; + + os::Function m_on_submit_action; + bool m_has_on_submit_action { false }; + }; +} diff --git a/libui/include/ui/TextInput.h b/libui/include/ui/TextInput.h new file mode 100644 index 00000000..6d38b3a7 --- /dev/null +++ b/libui/include/ui/TextInput.h @@ -0,0 +1,41 @@ +/** + * @file TextInput.h + * @author apio (cloudapio.eu) + * @brief Base class for text inputs. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#pragma once +#include +#include +#include + +namespace ui +{ + class TextInput : public Widget + { + public: + TextInput(); + + virtual Result handle_key_event(const ui::KeyEventRequest& request) = 0; + + virtual Result draw(ui::Canvas& canvas) = 0; + + protected: + Buffer m_data; + + usize m_cursor { 0 }; + ui::Point m_cursor_position { 0, 0 }; + + OwnedPtr m_cursor_timer; + bool m_cursor_activated = true; + + void tick_cursor(); + void update_cursor(); + + Result delete_current_character(); + Result insert_character(char c); + }; +} diff --git a/libui/src/InputField.cpp b/libui/src/InputField.cpp new file mode 100644 index 00000000..a1e00eeb --- /dev/null +++ b/libui/src/InputField.cpp @@ -0,0 +1,112 @@ +/** + * @file InputField.cpp + * @author apio (cloudapio.eu) + * @brief Single line text input widget. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include +#include +#include +#include +#include + +namespace ui +{ + InputField::InputField(SharedPtr font) : ui::TextInput(), m_font(font) + { + } + + Result InputField::handle_key_event(const ui::KeyEventRequest& request) + { + // Avoid handling "key released" events + if (!request.pressed) return ui::EventResult::DidNotHandle; + + if (request.code == moon::K_LeftArrow) + { + if (m_cursor > 0) m_cursor--; + else + return ui::EventResult::DidNotHandle; + update_cursor(); + return ui::EventResult::DidHandle; + } + + if (request.code == moon::K_RightArrow) + { + if (m_cursor < m_data.size()) m_cursor++; + else + return ui::EventResult::DidNotHandle; + update_cursor(); + return ui::EventResult::DidHandle; + } + + if (request.code == moon::K_Backspace) + { + if (m_cursor == 0) return ui::EventResult::DidNotHandle; + m_cursor--; + + delete_current_character(); + + update_cursor(); + + return ui::EventResult::DidHandle; + } + + if (request.letter == '\n') + { + if (m_has_on_submit_action) + { + m_on_submit_action(data()); + return ui::EventResult::DidHandle; + } + + return ui::EventResult::DidNotHandle; + } + + if (iscntrl(request.letter)) return ui::EventResult::DidNotHandle; + + if (m_cursor == m_data.size()) TRY(m_data.append_data((const u8*)&request.letter, 1)); + else + TRY(insert_character(request.letter)); + + m_cursor++; + + update_cursor(); + + return ui::EventResult::DidHandle; + } + + Result InputField::draw(ui::Canvas& canvas) + { + int visible_characters = canvas.width / m_font->width(); + + auto string = data(); + + Utf8StringDecoder decoder(string.chars()); + wchar_t buf[4096]; + decoder.decode(buf, sizeof(buf)).release_value(); + + int characters_to_render = (int)wcslen(buf); + + for (int j = 0; j < visible_characters && j < characters_to_render; j++) + { + auto subcanvas = canvas.subcanvas({ j * m_font->width(), 0, m_font->width(), m_font->height() }); + m_font->render(buf[j], ui::WHITE, subcanvas); + } + + // Draw the cursor + if ((int)m_cursor < visible_characters && m_cursor_activated) + { + canvas.subcanvas({ (int)m_cursor * m_font->width(), 0, 1, m_font->height() }).fill(ui::WHITE); + } + + return {}; + } + + StringView InputField::data() + { + return StringView { (const char*)m_data.data(), m_data.size() }; + } +} diff --git a/libui/src/TextInput.cpp b/libui/src/TextInput.cpp new file mode 100644 index 00000000..0e47a84a --- /dev/null +++ b/libui/src/TextInput.cpp @@ -0,0 +1,52 @@ +/** + * @file TextInput.cpp + * @author apio (cloudapio.eu) + * @brief Base class for text inputs. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include +#include + +namespace ui +{ + TextInput::TextInput() : Widget() + { + m_cursor_timer = os::Timer::create_repeating(500, [this]() { this->tick_cursor(); }).release_value(); + } + + void TextInput::update_cursor() + { + m_cursor_timer->restart(); + m_cursor_activated = true; + } + + Result TextInput::delete_current_character() + { + usize size = m_data.size() - m_cursor; + u8* slice = TRY(m_data.slice(m_cursor, size)); + memmove(slice, slice + 1, size - 1); + TRY(m_data.try_resize(m_data.size() - 1)); + + return {}; + } + + Result TextInput::insert_character(char c) + { + usize size = m_data.size() - m_cursor; + u8* slice = TRY(m_data.slice(m_cursor, size + 1)); + memmove(slice + 1, slice, size); + *slice = (u8)c; + + return {}; + } + + void TextInput::tick_cursor() + { + m_cursor_activated = !m_cursor_activated; + + window()->draw(); + } +}