From 4b37f1bb2fd00cd98da6a9f2ffd956c063eecfce Mon Sep 17 00:00:00 2001 From: Kyle Reed Date: Fri, 28 Apr 2023 19:26:53 -0700 Subject: [PATCH] Add a TestField widget that supports a cursor. --- firmware/application/ui/ui_alphanum.cpp | 17 +- firmware/application/ui/ui_alphanum.hpp | 1 - firmware/application/ui/ui_textentry.cpp | 213 +++++++++++++++++------ firmware/application/ui/ui_textentry.hpp | 59 +++++-- firmware/common/ui_painter.cpp | 4 +- firmware/common/ui_painter.hpp | 4 +- 6 files changed, 218 insertions(+), 80 deletions(-) diff --git a/firmware/application/ui/ui_alphanum.cpp b/firmware/application/ui/ui_alphanum.cpp index 16d2f300..f9161fc5 100644 --- a/firmware/application/ui/ui_alphanum.cpp +++ b/firmware/application/ui/ui_alphanum.cpp @@ -30,10 +30,6 @@ namespace ui { -void AlphanumView::paint(Painter&) { - draw_cursor(); -} - AlphanumView::AlphanumView( NavigationView& nav, std::string& str, @@ -41,7 +37,7 @@ AlphanumView::AlphanumView( ) : TextEntryView(nav, str, max_length) { size_t n; - + add_children({ &button_mode, &text_raw, @@ -77,16 +73,7 @@ AlphanumView::AlphanumView( field_raw.set_value('0'); field_raw.on_select = [this](NumberField&) { char_add(field_raw.value()); - update_text(); }; - - button_ok.on_select = [this, &nav](Button&) { - if (on_changed) - on_changed(_str); - nav.pop(); - }; - - update_text(); } void AlphanumView::set_mode(const uint32_t new_mode) { @@ -120,8 +107,6 @@ void AlphanumView::on_button(Button& button) { char_delete(); else char_add(c); - - update_text(); } bool AlphanumView::on_encoder(const EncoderEvent delta) { diff --git a/firmware/application/ui/ui_alphanum.hpp b/firmware/application/ui/ui_alphanum.hpp index 1e2e42ac..d61ee7fe 100644 --- a/firmware/application/ui/ui_alphanum.hpp +++ b/firmware/application/ui/ui_alphanum.hpp @@ -40,7 +40,6 @@ public: AlphanumView& operator=(const AlphanumView&) = delete; AlphanumView& operator=(AlphanumView&&) = delete; - void paint(Painter& painter) override; bool on_encoder(const EncoderEvent delta) override; private: diff --git a/firmware/application/ui/ui_textentry.cpp b/firmware/application/ui/ui_textentry.cpp index e4e008c1..d867ae62 100644 --- a/firmware/application/ui/ui_textentry.cpp +++ b/firmware/application/ui/ui_textentry.cpp @@ -45,76 +45,191 @@ void text_prompt(NavigationView& nav, std::string& str, const size_t max_length, }*/ } -void TextEntryView::update_text() { - if (cursor_pos < 30) - text_input.set(_str + std::string(_max_length - _str.length(), ' ')); - else - text_input.set('<' + _str.substr(cursor_pos - 29, 29)); - - draw_cursor(); +/* TextField ***********************************************************/ + +TextField::TextField( + std::string& str, + size_t max_length, + Point position, + uint32_t length +) : Widget{ { position, { 8 * static_cast(length), 16 } } }, + text_{ str }, + max_length_{ std::max(max_length, 1) }, + char_count_{ std::max(length, 1) }, + cursor_pos_{ text_.length() }, + insert_mode_{ true } +{ + set_focusable(true); } +const std::string& TextField::value() const { + return text_; +} + +void TextField::set(const std::string& str) { + // Assume that setting the string implies we want the whole thing. + max_length_ = std::max(max_length_, str.length()); + + text_ = str; + cursor_pos_ = str.length(); + set_cursor(str.length()); +} + +void TextField::set_cursor(uint32_t pos) { + cursor_pos_ = std::min(pos, text_.length()); + set_dirty(); +} + +void TextField::set_max_length(size_t max_length) { + // Doesn't make sense, ignore. + if (max_length == 0) + return; + + if (max_length < text_.length()) { + text_.erase(max_length - 1); + text_.shrink_to_fit(); + } else { + text_.reserve(max_length); + } + + max_length_ = max_length; + set_cursor(cursor_pos_); +} + +void TextField::set_insert_mode() { + insert_mode_ = true; +} + +void TextField::set_overwrite_mode() { + insert_mode_ = false; +} + +void TextField::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_) || + (cursor_pos_ >= text_.length() && !insert_mode_)) + return; + + if (insert_mode_) + text_.insert(cursor_pos_, 1, c); + else + text_[cursor_pos_] = c; + + cursor_pos_++; + set_dirty(); +} + +void TextField::char_delete() { + if (cursor_pos_ == 0) + return; + + cursor_pos_--; + text_.erase(cursor_pos_, 1); + set_dirty(); +} + +void TextField::paint(Painter& painter) { + constexpr int char_width = 8; + + auto rect = screen_rect(); + auto text_style = has_focus() ? style().invert() : style(); + auto offset = 0; + + // Does the string need to be shifted? + if (cursor_pos_ >= char_count_) + offset = cursor_pos_ - char_count_ + 1; + + // Clear the control. + painter.fill_rectangle(rect, text_style.background); + + // Draw the text starting at the offset. + for (uint32_t i = 0; i < char_count_ && i + offset < text_.length(); i++) { + painter.draw_char( + { rect.location().x() + (static_cast(i) * char_width), rect.location().y() }, + text_style, + text_[i + offset] + ); + } + + // Determine cursor position on screen (either the cursor position or the last char). + int32_t cursor_x = char_width * (offset > 0 ? char_count_ - 1 : cursor_pos_); + Point cursor_point{ screen_pos().x() + cursor_x, screen_pos().y() }; + auto cursor_style = text_style.invert(); + + // Invert the cursor character when in overwrite mode. + if (!insert_mode_ && (cursor_pos_) < text_.length()) + painter.draw_char(cursor_point, cursor_style, text_[cursor_pos_]); + + // Draw the cursor. + Rect cursor_box{ cursor_point, { char_width, 16 } }; + painter.draw_rectangle(cursor_box, cursor_style.background); +} + +bool TextField::on_key(const KeyEvent key) { + if (key == KeyEvent::Left && cursor_pos_ > 0) + cursor_pos_--; + else if (key == KeyEvent::Right && cursor_pos_ < text_.length()) + cursor_pos_++; + else if (key == KeyEvent::Select) + insert_mode_ = !insert_mode_; + else + return false; + + set_dirty(); + return true; +} + +bool TextField::on_encoder(const EncoderEvent delta) { + int32_t new_pos = cursor_pos_ + delta; + + // Let the encoder wrap around the ends of the text. + if (new_pos < 0) + new_pos = text_.length(); + else if (static_cast(new_pos) > text_.length()) + new_pos = 0; + + set_cursor(new_pos); + return true; +} + +bool TextField::on_touch(const TouchEvent event) { + if (event.type == TouchEvent::Type::Start) + focus(); + + set_dirty(); + return true; +} + +/* TextEntryView ***********************************************************/ + void TextEntryView::char_delete() { - if (!cursor_pos) return; - - cursor_pos--; - _str.resize(cursor_pos); + text_input.char_delete(); } void TextEntryView::char_add(const char c) { - if (cursor_pos >= _max_length) return; - - _str += c; - cursor_pos++; -} - -void TextEntryView::draw_cursor() { - Point draw_pos; - - draw_pos = { text_input.screen_rect().location().x() + std::min((Coord)cursor_pos, (Coord)28) * 8, - text_input.screen_rect().location().y() + 16 }; - - // Erase previous - display.fill_rectangle( - { { text_input.screen_rect().location().x(), draw_pos.y() }, { text_input.screen_rect().size().width(), 4 } }, - Color::black() - ); - // Draw new - display.fill_rectangle( - { draw_pos, { 8, 4 } }, - Color::white() - ); + text_input.char_add(c); } void TextEntryView::focus() { - button_ok.focus(); + text_input.focus(); } TextEntryView::TextEntryView( NavigationView& nav, std::string& str, size_t max_length -) : _str(str), - _max_length(max_length) +) : text_input{ str, max_length, { 0, 0 } } { - - // Trim from right - //_str->erase(std::find_if(_str->rbegin(), _str->rend(), std::not1(std::ptr_fun(std::isspace))).base(), _str->end()); - if (_str.length() > _max_length) - _str.resize(_max_length); - _str.reserve(_max_length); - - cursor_pos = _str.length(); - add_children({ &text_input, &button_ok }); - - button_ok.on_select = [this, &nav](Button&) { - _str.resize(cursor_pos); + + button_ok.on_select = [this, &str, &nav](Button&) { + str.shrink_to_fit(); // NB: str is the TextField string. if (on_changed) - on_changed(_str); + on_changed(str); nav.pop(); }; } diff --git a/firmware/application/ui/ui_textentry.hpp b/firmware/application/ui/ui_textentry.hpp index 9cd6f519..3641f2be 100644 --- a/firmware/application/ui/ui_textentry.hpp +++ b/firmware/application/ui/ui_textentry.hpp @@ -28,6 +28,54 @@ namespace ui { +// A TextField 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 { +public: + TextField(std::string& str, Point position, uint32_t length = 30) + : TextField{str, 64, position, length} { } + + // Str: the string containing the content to edit. + // Max_length: max length the string is allowed to use. + // Position: the top-left corner of the control. + // Length: the number of characters to display. + // - 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); + + TextField(const TextField&) = delete; + TextField(TextField&&) = delete; + TextField& operator=(const TextField&) = delete; + TextField& operator=(TextField&&) = delete; + + const std::string& value() const; + + void set(const std::string& str); + void set_cursor(uint32_t pos); + void set_max_length(size_t max_length); + void set_insert_mode(); + void set_overwrite_mode(); + + void char_add(char c); + void char_delete(); + + void paint(Painter& painter) override; + + bool on_key(const KeyEvent key) override; + bool on_encoder(const EncoderEvent delta) override; + bool on_touch(const TouchEvent event) override; + +protected: + std::string& text_; + size_t max_length_; + uint32_t char_count_; + uint32_t cursor_pos_; + bool insert_mode_; +}; + class TextEntryView : public View { public: std::function on_changed { }; @@ -45,17 +93,8 @@ protected: void char_add(const char c); void char_delete(); - void draw_cursor(); - void update_text(); - - std::string& _str; - size_t _max_length; - uint32_t cursor_pos { 0 }; - - Text text_input { - { 0, 0, 240, 16 } - }; + TextField text_input; Button button_ok { { 10 * 8, 33 * 8, 9 * 8, 32 }, "OK" diff --git a/firmware/common/ui_painter.cpp b/firmware/common/ui_painter.cpp index 2cb0d93f..b390668b 100644 --- a/firmware/common/ui_painter.cpp +++ b/firmware/common/ui_painter.cpp @@ -43,7 +43,7 @@ int Painter::draw_char(const Point p, const Style& style, const char c) { } int Painter::draw_string(Point p, const Font& font, const Color foreground, - const Color background, const std::string text) { + const Color background, const std::string& text) { bool escape = false; size_t width = 0; @@ -71,7 +71,7 @@ int Painter::draw_string(Point p, const Font& font, const Color foreground, return width; } -int Painter::draw_string(Point p, const Style& style, const std::string text) { +int Painter::draw_string(Point p, const Style& style, const std::string& text) { return draw_string(p, style.font, style.foreground, style.background, text); } diff --git a/firmware/common/ui_painter.hpp b/firmware/common/ui_painter.hpp index 369e3a57..0fb867c7 100644 --- a/firmware/common/ui_painter.hpp +++ b/firmware/common/ui_painter.hpp @@ -49,8 +49,8 @@ public: int draw_char(const Point p, const Style& style, const char c); int draw_string(Point p, const Font& font, const Color foreground, - const Color background, const std::string text); - int draw_string(Point p, const Style& style, const std::string text); + const Color background, const std::string& text); + int draw_string(Point p, const Style& style, const std::string& text); void draw_bitmap(const Point p, const Bitmap& bitmap, const Color background, const Color foreground);