From 25923e82a4e4cfa9e60ce1c8c0a914955d5968e7 Mon Sep 17 00:00:00 2001 From: Kyle Reed <3761006+kallanreed@users.noreply.github.com> Date: Fri, 14 Jul 2023 18:46:39 -0700 Subject: [PATCH] New Freqman edit UI (#1272) * WIP new edit UI * Fix textfield highlight * WIP new edit * Wrap up first pass of freqman edit * Fix indexing of options --------- Co-authored-by: kallanreed --- firmware/application/apps/ui_freqman.cpp | 203 ++++++++++++++++++++++- firmware/application/apps/ui_freqman.hpp | 95 +++++++++-- firmware/application/field_binder.hpp | 43 +++++ firmware/application/freqman_db.cpp | 58 ++++--- firmware/application/freqman_db.hpp | 3 + firmware/application/ui/ui_textentry.hpp | 2 +- firmware/common/ui_widget.cpp | 58 +++++-- firmware/common/ui_widget.hpp | 44 +++-- 8 files changed, 436 insertions(+), 70 deletions(-) create mode 100644 firmware/application/field_binder.hpp diff --git a/firmware/application/apps/ui_freqman.cpp b/firmware/application/apps/ui_freqman.cpp index 24856c66f..98c87c4e2 100644 --- a/firmware/application/apps/ui_freqman.cpp +++ b/firmware/application/apps/ui_freqman.cpp @@ -24,8 +24,12 @@ #include "ui_freqman.hpp" #include "event_m0.hpp" +#include "freqman.hpp" #include "portapack.hpp" #include "rtc_time.hpp" +#include "tone_key.hpp" +#include "ui_receiver.hpp" +#include "ui_styles.hpp" #include "utility.hpp" #include @@ -33,6 +37,10 @@ using namespace portapack; namespace fs = std::filesystem; +// TODO: Clean up after moving to better lookup tables. +extern ui::OptionsField::options_t freqman_bandwidths[4]; +extern ui::OptionsField::options_t freqman_steps; + namespace ui { /* FreqManBaseView ***************************************/ @@ -188,8 +196,15 @@ FrequencyLoadView::FrequencyLoadView( /* FrequencyManagerView **********************************/ +void FrequencyManagerView::on_edit_entry() { + auto edit_view = nav_.push(current_entry()); + edit_view->on_save = [this](const freqman_entry& entry) { + db_.replace_entry(current_index(), entry); + freqlist_view.set_dirty(); + }; +} + void FrequencyManagerView::on_edit_freq() { - // TODO: range edit support. auto freq_edit_view = nav_.push(current_entry().frequency_a); freq_edit_view->on_changed = [this](rf::Frequency f) { auto entry = current_entry(); @@ -263,20 +278,21 @@ FrequencyManagerView::FrequencyManagerView( : FreqManBaseView(nav) { add_children( {&freqlist_view, - &labels, &button_add_category, &button_del_category, + &button_edit_entry, + &rect_padding, &button_edit_freq, &button_edit_desc, &button_add_entry, &button_del_entry}); freqlist_view.on_select = [this](size_t) { - button_edit_freq.focus(); + button_edit_entry.focus(); }; // Allows for quickly exiting control. freqlist_view.on_leave = [this]() { - button_edit_freq.focus(); + button_edit_entry.focus(); }; button_add_category.on_select = [this]() { @@ -287,6 +303,10 @@ FrequencyManagerView::FrequencyManagerView( on_del_category(); }; + button_edit_entry.on_select = [this](Button&) { + on_edit_entry(); + }; + button_edit_freq.on_select = [this](Button&) { on_edit_freq(); }; @@ -304,4 +324,179 @@ FrequencyManagerView::FrequencyManagerView( }; } +/* FrequencyEditView *************************************/ + +FrequencyEditView::FrequencyEditView( + NavigationView& nav, + freqman_entry entry) + : nav_{nav}, + entry_{std::move(entry)} { + add_children({&labels, + &field_type, + &field_freq_a, + &field_freq_b, + &field_modulation, + &field_bandwidth, + &field_step, + &field_tone, + &field_description, + &text_validation, + &button_save, + &button_cancel}); + + freqman_set_modulation_option(field_modulation); + populate_bandwidth_options(); + populate_step_options(); + populate_tone_options(); + + // Add the "invalid/unset" option. Kind of hacky, but... + field_modulation.options().insert( + field_modulation.options().begin(), {"None", -1}); + field_step.options().insert( + field_step.options().begin(), {"None", -1}); + + field_type.set_by_value((int32_t)entry_.type); + field_type.on_change = [this](size_t, auto value) { + entry_.type = static_cast(value); + refresh_ui(); + }; + + // TODO: this pattern should be able to be wrapped up. + field_freq_a.set_value(entry_.frequency_a); + field_freq_a.on_change = [this](rf::Frequency f) { + entry_.frequency_a = f; + refresh_ui(); + }; + field_freq_a.on_edit = [this]() { + auto freq_view = nav_.push(field_freq_a.value()); + freq_view->on_changed = [this](rf::Frequency f) { + field_freq_a.set_value(f); + }; + }; + + field_freq_b.set_value(entry_.frequency_b); + field_freq_b.on_change = [this](rf::Frequency f) { + entry_.frequency_b = f; + refresh_ui(); + }; + field_freq_b.on_edit = [this]() { + auto freq_view = nav_.push(field_freq_b.value()); + freq_view->on_changed = [this](rf::Frequency f) { + field_freq_b.set_value(f); + }; + }; + + field_modulation.set_by_value((int32_t)entry_.modulation); + field_modulation.on_change = [this](size_t, auto value) { + entry_.modulation = static_cast(value); + populate_bandwidth_options(); + }; + + field_bandwidth.set_by_value((int32_t)entry_.bandwidth); + field_bandwidth.on_change = [this](size_t, auto value) { + entry_.bandwidth = static_cast(value); + }; + + field_step.set_by_value((int32_t)entry_.step); + field_step.on_change = [this](size_t, auto value) { + entry_.step = static_cast(value); + }; + + field_tone.set_by_value((int32_t)entry_.tone); + field_tone.on_change = [this](size_t, auto value) { + entry_.tone = static_cast(value); + }; + + field_description.set_text(entry_.description); + field_description.on_change = [this](TextField& tf) { + entry_.description = tf.get_text(); + }; + field_description.on_select = [this](TextField& tf) { + temp_buffer_ = tf.get_text(); + text_prompt(nav_, temp_buffer_, FreqManBaseView::desc_edit_max, + [this, &tf](std::string& new_desc) { + tf.set_text(new_desc); + }); + }; + + button_save.on_select = [this](Button&) { + if (on_save) + on_save(std::move(entry_)); + nav_.pop(); + }; + + button_cancel.on_select = [this](Button&) { + nav_.pop(); + }; + + refresh_ui(); +} + +void FrequencyEditView::focus() { + button_cancel.focus(); +} + +void FrequencyEditView::refresh_ui() { + // This needs to be called whenever a UI option is changed + // in a way that causes fields to be unused or would make the + // entry fail validation. + + auto is_range = entry_.type == freqman_type::Range; + auto is_ham = entry_.type == freqman_type::HamRadio; + auto has_freq_b = is_range || is_ham; + + field_freq_b.set_style(has_freq_b ? &Styles::white : &Styles::grey); + field_step.set_style(is_range ? &Styles::white : &Styles::grey); + field_tone.set_style(is_ham ? &Styles::white : &Styles::grey); + + if (is_valid(entry_)) { + text_validation.set("Valid"); + text_validation.set_style(&Styles::green); + } else { + text_validation.set("Error"); + text_validation.set_style(&Styles::red); + } +} + +// TODO: The following functions shouldn't be needed once +// freqman_db lookup tables are complete. +void FrequencyEditView::populate_bandwidth_options() { + OptionsField::options_t options; + options.push_back({"None", -1}); + + if (entry_.modulation < std::size(freqman_bandwidths)) { + auto& bandwidths = freqman_bandwidths[entry_.modulation]; + for (auto i = 1u; i < bandwidths.size(); ++i) { + auto& item = bandwidths[i]; + options.push_back({item.first, (OptionsField::value_t)i}); + } + } + + field_bandwidth.set_options(std::move(options)); +} + +void FrequencyEditView::populate_step_options() { + OptionsField::options_t options; + options.push_back({"None", -1}); + + for (auto i = 1u; i < freqman_steps.size(); ++i) { + auto& item = freqman_steps[i]; + options.push_back({item.first, (OptionsField::value_t)i}); + } + + field_step.set_options(std::move(options)); +} + +void FrequencyEditView::populate_tone_options() { + OptionsField::options_t options; + options.push_back({"None", -1}); + + for (auto i = 1u; i < tonekey::tone_keys.size(); ++i) { + auto& item = tonekey::tone_keys[i]; + options.push_back({item.first, (OptionsField::value_t)i}); + } + + field_tone.set_options(std::move(options)); +} + } // namespace ui diff --git a/firmware/application/apps/ui_freqman.hpp b/firmware/application/apps/ui_freqman.hpp index d32e6c945..a93e48468 100644 --- a/firmware/application/apps/ui_freqman.hpp +++ b/firmware/application/apps/ui_freqman.hpp @@ -36,17 +36,18 @@ namespace ui { class FreqManBaseView : public View { public: - FreqManBaseView( - NavigationView& nav); + FreqManBaseView(NavigationView& nav); void focus() override; + static constexpr size_t desc_edit_max = 0x80; + protected: using options_t = OptionsField::options_t; NavigationView& nav_; freqman_error error_{NO_ERROR}; - std::function on_select_frequency{nullptr}; + std::function on_select_frequency{}; void change_category(size_t new_index); /* Access the categories directly from the OptionsField. @@ -79,14 +80,13 @@ class FreqManBaseView : public View { protected: /* Static so selected category is persisted across UI instances. */ static size_t current_category_index; - static constexpr size_t desc_edit_max = 0x80; }; // TODO: support for new category. class FrequencySaveView : public FreqManBaseView { public: FrequencySaveView(NavigationView& nav, const rf::Frequency value); - std::string title() const override { return "Save freq"; }; + std::string title() const override { return "Save freq"; } private: std::string temp_buffer_{}; @@ -122,17 +122,18 @@ class FrequencyLoadView : public FreqManBaseView { std::function on_range_loaded{}; FrequencyLoadView(NavigationView& nav); - std::string title() const override { return "Load freq"; }; + std::string title() const override { return "Load freq"; } }; class FrequencyManagerView : public FreqManBaseView { public: FrequencyManagerView(NavigationView& nav); - std::string title() const override { return "Freqman"; }; + std::string title() const override { return "Freqman"; } private: std::string temp_buffer_{}; + void on_edit_entry(); void on_edit_freq(); void on_edit_desc(); void on_add_category(); @@ -140,9 +141,6 @@ class FrequencyManagerView : public FreqManBaseView { void on_add_entry(); void on_del_entry(); - Labels labels{ - {{5 * 8, 14 * 16 - 4}, "Edit:", Color::light_grey()}}; - NewButton button_add_category{ {23 * 8, 0 * 16, 7 * 4, 20}, {}, @@ -157,6 +155,14 @@ class FrequencyManagerView : public FreqManBaseView { Color::red(), true}; + Button button_edit_entry{ + {0 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4}, + "Edit"}; + + Rectangle rect_padding{ + {15 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4}, + Color::grey()}; + Button button_edit_freq{ {0 * 8, 15 * 16, 15 * 8, 2 * 16}, "Frequency"}; @@ -180,4 +186,73 @@ class FrequencyManagerView : public FreqManBaseView { true}; }; +class FrequencyEditView : public View { + public: + std::function on_save{}; + + FrequencyEditView( + NavigationView& nav, + freqman_entry entry); + std::string title() const override { return "Freqman Edit"; } + + void focus() override; + + private: + NavigationView& nav_; + freqman_entry entry_; + std::string temp_buffer_{}; + + void refresh_ui(); + void populate_bandwidth_options(); + void populate_step_options(); + void populate_tone_options(); + + Labels labels{ + {{5 * 8, 1 * 16}, "Edit Frequency Entry", Color::white()}, + {{0 * 8, 3 * 16}, "Entry Type :", Color::light_grey()}, + {{0 * 8, 4 * 16}, "Frequency A:", Color::light_grey()}, + {{0 * 8, 5 * 16}, "Frequency B:", Color::light_grey()}, + {{0 * 8, 6 * 16}, "Modulation :", Color::light_grey()}, + {{0 * 8, 7 * 16}, "Bandwidth :", Color::light_grey()}, + {{0 * 8, 8 * 16}, "Step :", Color::light_grey()}, + {{0 * 8, 9 * 16}, "Tone Freq :", Color::light_grey()}, + {{0 * 8, 10 * 16}, "Description:", Color::light_grey()}, + }; + + OptionsField field_type{{13 * 8, 3 * 16}, 8, { + {"Single", 0}, + {"Range", 1}, + {"HamRadio", 2}, + {"Raw", 3}, + }}; + + FrequencyField field_freq_a{{13 * 8, 4 * 16}}; + + FrequencyField field_freq_b{{13 * 8, 5 * 16}}; + + OptionsField field_modulation{{13 * 8, 6 * 16}, 5, {}}; + + OptionsField field_bandwidth{{13 * 8, 7 * 16}, 7, {}}; + + OptionsField field_step{{13 * 8, 8 * 16}, 12, {}}; + + OptionsField field_tone{{13 * 8, 9 * 16}, 13, {}}; + + TextField field_description{ + {13 * 8, 10 * 16, 17 * 8, 1 * 16}, + {}}; + + Text text_validation{ + {12 * 8, 12 * 16, 5 * 8, 1 * 16}, + {}}; + + Button button_save{ + {0 * 8, 17 * 16, 15 * 8, 2 * 16}, + "Save"}; + + Button button_cancel{ + {15 * 8, 17 * 16, 15 * 8, 2 * 16}, + "Cancel"}; +}; + } /* namespace ui */ diff --git a/firmware/application/field_binder.hpp b/firmware/application/field_binder.hpp new file mode 100644 index 000000000..d95ebf145 --- /dev/null +++ b/firmware/application/field_binder.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __FIELD_BINDER_H__ +#define __FIELD_BINDER_H__ + +/* There's a common pattern that appears in applications that use + * field widgets. The field value is set, on_change and on_select + * handlers are registered. on_select will read the value and + * set the new value once changed. on_change will be called which + * will write the new value to the backing variable. */ + +struct binding_traits { +}; + +template +constexpr auto get_traits() { + return {}; +} + +template ()> +class FieldBinding { +}; + +#endif /*__FIELD_BINDER_H__*/ diff --git a/firmware/application/freqman_db.cpp b/firmware/application/freqman_db.cpp index 06755a170..ae0fac442 100644 --- a/firmware/application/freqman_db.cpp +++ b/firmware/application/freqman_db.cpp @@ -97,6 +97,7 @@ options_t freqman_bandwidths[4] = { }, }; +// TODO: these should be indexes. options_t freqman_steps = { {"0.1kHz ", 100}, {"1kHz ", 1000}, @@ -116,6 +117,7 @@ options_t freqman_steps = { {"1MHz ", 1000000}, }; +// TODO: these should be indexes. options_t freqman_steps_short = { {"0.1kHz", 100}, {"1kHz", 1000}, @@ -337,32 +339,7 @@ bool parse_freqman_entry(std::string_view str, freqman_entry& entry) { } } - // No valid frequency combination was set. - if (entry.type == freqman_type::Unknown) - return false; - - // Frequency A must be set for all types - if (entry.frequency_a == 0) - return false; - - // Frequency B must be set for type Range or Ham Radio - if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) { - if (entry.frequency_b == 0) - return false; - } - - // Ranges should have frequencies A <= B. - if (entry.type == freqman_type::Range) { - if (entry.frequency_a > entry.frequency_b) - return false; - } - - // TODO: Consider additional validation: - // - Tone only on HamRadio. - // - Fail on failed parse_int. - // - Fail if bandwidth set before modulation. - - return true; + return is_valid(entry); } // TODO: Use FreqmanDB iterator. @@ -411,6 +388,35 @@ bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_optio return true; } +bool is_valid(const freqman_entry& entry) { + // No valid frequency combination was set. + if (entry.type == freqman_type::Unknown) + return false; + + // Frequency A must be set for all types + if (entry.frequency_a == 0) + return false; + + // Frequency B must be set for type Range or Ham Radio + if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) { + if (entry.frequency_b == 0) + return false; + } + + // Ranges should have frequencies A <= B. + if (entry.type == freqman_type::Range) { + if (entry.frequency_a > entry.frequency_b) + return false; + } + + // TODO: Consider additional validation: + // - Tone only on HamRadio. + // - Fail on failed parse_int. + // - Fail if bandwidth set before modulation. + + return true; +} + /* FreqmanDB ***********************************/ bool FreqmanDB::open(const std::filesystem::path& path) { diff --git a/firmware/application/freqman_db.hpp b/firmware/application/freqman_db.hpp index 6058fd834..e51de13e2 100644 --- a/firmware/application/freqman_db.hpp +++ b/firmware/application/freqman_db.hpp @@ -182,6 +182,9 @@ std::string to_freqman_string(const freqman_entry& entry); bool parse_freqman_entry(std::string_view str, freqman_entry& entry); bool parse_freqman_file(const std::filesystem::path& path, freqman_db& db, freqman_load_options options); +/* Returns true if the entry is well-formed. */ +bool is_valid(const freqman_entry& entry); + /* The tricky part of using the file directly is that there can be comments * and empty lines in the file. This messes up the 'count' calculation. * Either have to live with 'count' being an upper bound have the callers diff --git a/firmware/application/ui/ui_textentry.hpp b/firmware/application/ui/ui_textentry.hpp index c7d6ab03a..16e106280 100644 --- a/firmware/application/ui/ui_textentry.hpp +++ b/firmware/application/ui/ui_textentry.hpp @@ -48,7 +48,7 @@ class TextEntryView : public View { void char_add(const char c); void char_delete(); - TextField text_input; + TextEdit text_input; Button button_ok{ {22 * 8, 33 * 8, 7 * 8, 32}, "OK"}; diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 744d3f5ff..16b85dcc4 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -349,7 +349,7 @@ Text::Text( Rect parent_rect, std::string text) : Widget{parent_rect}, - text{text} { + text{std::move(text)} { } Text::Text( @@ -357,14 +357,14 @@ Text::Text( : Text{parent_rect, {}} { } -void Text::set(const std::string value) { - text = value; +void Text::set(std::string_view value) { + text = std::string{value}; set_dirty(); } void Text::paint(Painter& painter) { const auto rect = screen_rect(); - const auto s = style(); + auto s = has_focus() ? style().invert() : style(); painter.fill_rectangle(rect, s.background); @@ -1608,9 +1608,9 @@ bool OptionsField::on_touch(const TouchEvent event) { return true; } -/* TextField ***********************************************************/ +/* TextEdit ***********************************************************/ -TextField::TextField( +TextEdit::TextEdit( std::string& str, size_t max_length, Point position, @@ -1624,24 +1624,24 @@ TextField::TextField( set_focusable(true); } -const std::string& TextField::value() const { +const std::string& TextEdit::value() const { return text_; } -void TextField::set_cursor(uint32_t pos) { +void TextEdit::set_cursor(uint32_t pos) { cursor_pos_ = std::min(pos, text_.length()); set_dirty(); } -void TextField::set_insert_mode() { +void TextEdit::set_insert_mode() { insert_mode_ = true; } -void TextField::set_overwrite_mode() { +void TextEdit::set_overwrite_mode() { insert_mode_ = false; } -void TextField::char_add(char c) { +void TextEdit::char_add(char c) { // Don't add if inserting and at max_length and // don't overwrite if past the end of the text. if ((text_.length() >= max_length_ && insert_mode_) || @@ -1657,7 +1657,7 @@ void TextField::char_add(char c) { set_dirty(); } -void TextField::char_delete() { +void TextEdit::char_delete() { if (cursor_pos_ == 0) return; @@ -1666,7 +1666,7 @@ void TextField::char_delete() { set_dirty(); } -void TextField::paint(Painter& painter) { +void TextEdit::paint(Painter& painter) { constexpr int char_width = 8; auto rect = screen_rect(); @@ -1702,7 +1702,7 @@ void TextField::paint(Painter& painter) { painter.draw_rectangle(cursor_box, cursor_style.background); } -bool TextField::on_key(const KeyEvent key) { +bool TextEdit::on_key(const KeyEvent key) { if (key == KeyEvent::Left && cursor_pos_ > 0) cursor_pos_--; else if (key == KeyEvent::Right && cursor_pos_ < text_.length()) @@ -1716,7 +1716,7 @@ bool TextField::on_key(const KeyEvent key) { return true; } -bool TextField::on_encoder(const EncoderEvent delta) { +bool TextEdit::on_encoder(const EncoderEvent delta) { int32_t new_pos = cursor_pos_ + delta; // Let the encoder wrap around the ends of the text. @@ -1729,7 +1729,7 @@ bool TextField::on_encoder(const EncoderEvent delta) { return true; } -bool TextField::on_touch(const TouchEvent event) { +bool TextEdit::on_touch(const TouchEvent event) { if (event.type == TouchEvent::Type::Start) focus(); @@ -1737,6 +1737,32 @@ bool TextField::on_touch(const TouchEvent event) { return true; } +/* TextField *************************************************************/ + +TextField::TextField(Rect parent_rect, std::string text) + : Text(parent_rect, std::move(text)) { + set_focusable(true); +} + +const std::string& TextField::get_text() const { + return text; +} + +void TextField::set_text(std::string_view value) { + set(value); + if (on_change) + on_change(*this); +} + +bool TextField::on_key(KeyEvent key) { + if (key == KeyEvent::Select && on_select) { + on_select(*this); + return true; + } + + return false; +} + /* NumberField ***********************************************************/ NumberField::NumberField( diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 819f8f0bc..c25587664 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -33,10 +33,11 @@ #include "portapack.hpp" #include "utility.hpp" +#include #include -#include #include -#include +#include +#include namespace ui { @@ -202,11 +203,11 @@ class Text : public Widget { Text(Rect parent_rect, std::string text); Text(Rect parent_rect); - void set(const std::string value); + void set(std::string_view value); void paint(Painter& painter) override; - private: + protected: std::string text; }; @@ -619,6 +620,7 @@ class OptionsField : public Widget { OptionsField(Point parent_pos, size_t length, options_t options); + options_t& options() { return options_; } const options_t& options() const { return options_; } void set_options(options_t new_options); @@ -642,14 +644,14 @@ class OptionsField : public Widget { size_t selected_index_{0}; }; -// A TextField is bound to a string reference and allows the string +// A TextEdit is bound to a string reference and allows the string // to be manipulated. The field itself does not provide the UI for // setting the value. It provides the UI of rendering the text, // a cursor, and an API to edit the string content. -class TextField : public Widget { +class TextEdit : public Widget { public: - TextField(std::string& str, Point position, uint32_t length = 30) - : TextField{str, 64, position, length} {} + TextEdit(std::string& str, Point position, uint32_t length = 30) + : TextEdit{str, 64, position, length} {} // Str: the string containing the content to edit. // Max_length: max length the string is allowed to use. @@ -658,12 +660,12 @@ class TextField : public Widget { // - Characters are 8 pixels wide. // - The screen can show 30 characters max. // - The control is 16 pixels tall. - TextField(std::string& str, size_t max_length, Point position, uint32_t length = 30); + TextEdit(std::string& str, size_t max_length, Point position, uint32_t length = 30); - TextField(const TextField&) = delete; - TextField(TextField&&) = delete; - TextField& operator=(const TextField&) = delete; - TextField& operator=(TextField&&) = delete; + TextEdit(const TextEdit&) = delete; + TextEdit(TextEdit&&) = delete; + TextEdit& operator=(const TextEdit&) = delete; + TextEdit& operator=(TextEdit&&) = delete; const std::string& value() const; @@ -688,6 +690,22 @@ class TextField : public Widget { bool insert_mode_; }; +class TextField : public Text { + public: + std::function on_select{}; + std::function on_change{}; + + TextField(Rect parent_rect, std::string text); + + const std::string& get_text() const; + void set_text(std::string_view value); + + bool on_key(KeyEvent key) override; + + private: + using Text::set; +}; + class NumberField : public Widget { public: std::function on_select{};