From 00667cecf95cafbe6608e2ca6515f15edaa29410 Mon Sep 17 00:00:00 2001 From: Kyle Reed Date: Fri, 26 May 2023 01:02:17 -0700 Subject: [PATCH] Notepad menu (#1072) * WIP notepad menu --- firmware/application/apps/ui_text_editor.cpp | 347 +++++++++++++------ firmware/application/apps/ui_text_editor.hpp | 232 ++++++++++--- firmware/application/bitmap.hpp | 60 ++-- firmware/application/file.cpp | 5 +- firmware/common/optional.hpp | 1 + firmware/graphics/icon_save.png | Bin 171 -> 188 bytes firmware/test/application/test_optional.cpp | 19 + 7 files changed, 472 insertions(+), 192 deletions(-) diff --git a/firmware/application/apps/ui_text_editor.cpp b/firmware/application/apps/ui_text_editor.cpp index f33ed6b6..8dcdcb3f 100644 --- a/firmware/application/apps/ui_text_editor.cpp +++ b/firmware/application/apps/ui_text_editor.cpp @@ -21,6 +21,7 @@ #include "ui_fileman.hpp" #include "ui_text_editor.hpp" +#include "ui_textentry.hpp" #include "log_file.hpp" #include "string_format.hpp" @@ -29,11 +30,6 @@ using namespace portapack; namespace fs = std::filesystem; namespace { -template -T mid(const T& val1, const T& val2, const T& val3) { - return std::max(val1, std::min(val2, val3)); -} - /*void log(const std::string& msg) { LogFile log{}; log.append("LOGS/Notepad.txt"); @@ -50,12 +46,12 @@ FileWrapper::FileWrapper() { Optional FileWrapper::open(const fs::path& path) { file_ = File(); - auto result = file_.open(path); + auto error = file_.open(path); - if (!result.is_valid()) // No error + if (!error) initialize(); - return result; + return error; } std::string FileWrapper::get_text(Offset line, Offset col, Offset length) { @@ -63,7 +59,7 @@ std::string FileWrapper::get_text(Offset line, Offset col, Offset length) { auto range = line_range(line); int32_t to_read = length; - if (!range.is_valid()) + if (!range) return "[UNCACHED LINE]"; // Don't read past end of line. @@ -80,7 +76,7 @@ Optional FileWrapper::line_range(Line line) { ensure_cached(line); auto offset = offset_for_line(line); - if (!offset.is_valid()) + if (!offset) return {}; auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1); @@ -92,7 +88,7 @@ Optional FileWrapper::line_range(Line line) { FileWrapper::Offset FileWrapper::line_length(Line line) { auto range = line_range(line); - if (range.is_valid()) + if (range) return range->end - range->start; return 0; @@ -108,7 +104,7 @@ void FileWrapper::initialize() { Offset offset = 0; auto result = next_newline(offset); - while (result.is_valid()) { + while (result) { ++line_count_; if (newlines_.size() < max_newlines) newlines_.push_back(*result); @@ -150,7 +146,7 @@ void FileWrapper::ensure_cached(Line line) { return; auto result = offset_for_line(line); - if (result.is_valid()) + if (result) return; if (line < start_line_) { @@ -162,7 +158,7 @@ void FileWrapper::ensure_cached(Line line) { auto offset = previous_newline(start_offset_ - 2); newlines_.push_front(start_offset_ - 1); - if (!offset.is_valid()) { + if (!offset) { // Must be at beginning. start_line_ = 0; start_offset_ = 0; @@ -176,7 +172,7 @@ void FileWrapper::ensure_cached(Line line) { } else { while (line >= start_line_ + newlines_.size()) { auto offset = next_newline(newlines_.back() + 1); - if (offset.is_valid()) { + if (offset) { start_line_++; start_offset_ = newlines_.front() + 1; newlines_.push_back(*offset); @@ -186,7 +182,6 @@ void FileWrapper::ensure_cached(Line line) { } Optional FileWrapper::previous_newline(Offset start) { - constexpr size_t buffer_size = 128; char buffer[buffer_size]; Offset offset = start; auto to_read = buffer_size; @@ -222,7 +217,6 @@ Optional FileWrapper::previous_newline(Offset start) { } Optional FileWrapper::next_newline(Offset start) { - constexpr size_t buffer_size = 128; char buffer[buffer_size]; Offset offset = start; @@ -255,41 +249,20 @@ Optional FileWrapper::next_newline(Offset start) { return {offset}; } -/* TextEditorView ***************************************************/ +/* TextViewer *******************************************************/ -TextEditorView::TextEditorView(NavigationView& nav) - : nav_{nav} { - add_children( - { - &button_open, - &text_position, - &text_size, - }); +TextViewer::TextViewer(Rect parent_rect) + : Widget(parent_rect), + max_line{static_cast(parent_rect.height() / char_height)}, + max_col{static_cast(parent_rect.width() / char_width)} { set_focusable(true); - - button_open.on_select = [this](Button&) { - auto open_view = nav_.push(""); - open_view->on_changed = [this](std::filesystem::path path) { - open_file(path); - }; - }; } -TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path) - : TextEditorView(nav) { - open_file(path); -} - -void TextEditorView::on_focus() { - refresh_ui(); - button_open.focus(); -} - -void TextEditorView::paint(Painter& painter) { +void TextViewer::paint(Painter& painter) { auto first_line = paint_state_.first_line; auto first_col = paint_state_.first_col; - if (!paint_state_.has_file) + if (!has_file()) return; // Move the viewport vertically. @@ -320,7 +293,7 @@ void TextEditorView::paint(Painter& painter) { paint_cursor(painter); } -bool TextEditorView::on_key(const KeyEvent key) { +bool TextViewer::on_key(const KeyEvent key) { int16_t delta_col = 0; int16_t delta_line = 0; @@ -332,19 +305,22 @@ bool TextEditorView::on_key(const KeyEvent key) { delta_line = -1; else if (key == KeyEvent::Down) delta_line = 1; - /* else if (key == KeyEvent::Select) - ; // TODO: Edit/Menu */ + else if (key == KeyEvent::Select && on_select) { + on_select(); + return true; + } // Always allow cursor direction to be updated. cursor_.dir = delta_col != 0 ? ScrollDirection::Horizontal : ScrollDirection::Vertical; auto updated = apply_scrolling_constraints(delta_line, delta_col); if (updated) - refresh_ui(); + redraw(); + return updated; } -bool TextEditorView::on_encoder(EncoderEvent delta) { +bool TextViewer::on_encoder(EncoderEvent delta) { bool updated = false; if (cursor_.dir == ScrollDirection::Horizontal) @@ -353,15 +329,18 @@ bool TextEditorView::on_encoder(EncoderEvent delta) { updated = apply_scrolling_constraints(delta, 0); if (updated) - refresh_ui(); + redraw(); return updated; } -bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) { +bool TextViewer::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) { + if (!has_file()) + return false; + int32_t new_line = cursor_.line + delta_line; int32_t new_col = cursor_.col + delta_col; - int32_t new_line_length = file_.line_length(new_line); + int32_t new_line_length = file_->line_length(new_line); if (new_col < 0) --new_line; @@ -375,9 +354,9 @@ bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t del if (new_line < 0 && new_col > 0) { new_line = 0; new_col = 0; - } else if (new_line >= (int32_t)file_.line_count()) { - auto last_line = file_.line_count() - 1; - int32_t last_col = file_.line_length(last_line) - 1; + } else if (new_line >= (int32_t)file_->line_count()) { + auto last_line = file_->line_count() - 1; + int32_t last_col = file_->line_length(last_line) - 1; if (new_col < last_col) { new_line = last_line; @@ -385,10 +364,10 @@ bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t del } } - if (new_line < 0 || (uint32_t)new_line >= file_.line_count()) + if (new_line < 0 || (uint32_t)new_line >= file_->line_count()) return false; - new_line_length = file_.line_length(new_line); + new_line_length = file_->line_length(new_line); // TODO: don't wrap with encoder? // Wrap or clamp column. @@ -400,75 +379,51 @@ bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t del cursor_.line = new_line; cursor_.col = new_col; + if (on_cursor_moved) + on_cursor_moved(); + return true; } -void TextEditorView::refresh_ui() { - if (paint_state_.has_file) { - text_position.set( - "Ln " + to_string_dec_uint(cursor_.line + 1) + - ", Col " + to_string_dec_uint(cursor_.col + 1)); - text_size.set( - "Lines:" + to_string_dec_uint(file_.line_count()) + - " (" + to_string_file_size(file_.size()) + ")"); - } else { - // if (!button_open.has_focus()) - // button_open.focus(); - text_position.set(""); - text_size.set(""); - } - +void TextViewer::redraw(bool redraw_text) { + paint_state_.redraw_text = redraw_text; set_dirty(); } -void TextEditorView::open_file(const fs::path& path) { - // TODO: need a temp backing file for edits. - auto result = file_.open(path); - - if (result.is_valid()) - nav_.display_modal("Read Error", "Cannot open file:\n" + result->what()); - - paint_state_.has_file = !result.is_valid(); // Has an error. - paint_state_.first_line = 0; - paint_state_.first_col = 0; - cursor_.line = 0; - cursor_.col = 0; - - paint_state_.redraw_text = true; - refresh_ui(); -} - -void TextEditorView::paint_text(Painter& painter, uint32_t line, uint16_t col) { - // TODO: A line cache would use more memory but save a lot of IO. +void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) { + // CONSIDER: A line cache would use more memory but save a lot of IO. // Only the new lines/characters would need to be refetched. auto r = screen_rect(); // Draw the lines from the file for (auto i = 0u; i < max_line; ++i) { - if (line + i >= file_.line_count()) + if (line + i >= file_->line_count()) break; - auto str = file_.get_text(line + i, col, max_col); + auto str = file_->get_text(line + i, col, max_col); // Draw text. if (str.length() > 0) painter.draw_string( - {0, r.location().y() + (int)i * char_height}, - style_default, str); + {0, r.top() + (int)i * char_height}, + style_text, str); // Clear empty line sections. int32_t clear_width = max_col - str.length(); if (clear_width > 0) painter.fill_rectangle( {(max_col - clear_width) * char_width, - r.location().y() + (int)i * char_height, + r.top() + (int)i * char_height, clear_width * char_width, char_height}, - style_default.background); + style_text.background); } } -void TextEditorView::paint_cursor(Painter& painter) { +void TextViewer::paint_cursor(Painter& painter) { + if (!has_focus()) + return; + auto draw_cursor = [this, &painter](uint32_t line, uint16_t col, Color c) { auto r = screen_rect(); line = line - paint_state_.first_line; @@ -476,21 +431,201 @@ void TextEditorView::paint_cursor(Painter& painter) { painter.draw_rectangle( {(int)col * char_width - 1, - r.location().y() + (int)line * char_height, + r.top() + (int)line * char_height, char_width + 1, char_height}, c); }; - // TODO: XOR cursor? - // Clear old cursor. - draw_cursor(paint_state_.line, paint_state_.col, style_default.background); - draw_cursor(cursor_.line, cursor_.col, style_default.foreground); + // Clear old cursor. CONSIDER: XOR cursor? + draw_cursor(paint_state_.line, paint_state_.col, style_text.background); + draw_cursor(cursor_.line, cursor_.col, style_text.foreground); paint_state_.line = cursor_.line; paint_state_.col = cursor_.col; } -uint16_t TextEditorView::line_length() { - return file_.line_length(cursor_.line); +void TextViewer::reset_file(FileWrapper* file) { + file_ = file; + paint_state_.first_line = 0; + paint_state_.first_col = 0; + cursor_.line = 0; + cursor_.col = 0; + redraw(true); +} + +uint16_t TextViewer::line_length() { + return file_->line_length(cursor_.line); +} + +/* TextEditorMenu ***************************************************/ + +TextEditorMenu::TextEditorMenu() + : View{{7 * 4, 9 * 4, 25 * 8, 25 * 8}} { + add_children( + { + &rect_frame, + &button_cut, + &button_paste, + &button_copy, + &button_delline, + &button_edit, + &button_addline, + &button_open, + &button_save, + &button_exit, + }); +} + +void TextEditorMenu::on_show() { + hide_children(false); + button_edit.focus(); +} + +void TextEditorMenu::on_hide() { + hide_children(true); +} + +void TextEditorMenu::hide_children(bool hidden) { + for (auto child : children()) { + child->hidden(hidden); + } +} + +/* TextEditorView ***************************************************/ + +TextEditorView::TextEditorView(NavigationView& nav) + : nav_{nav} { + add_children( + { + &viewer, + &menu, + &button_menu, + &text_position, + &text_size, + }); + + viewer.on_select = [this]() { + // Treat as if menu button was pressed. + if (button_menu.on_select) + button_menu.on_select(); + }; + + viewer.on_cursor_moved = [this]() { + update_position(); + }; + + menu.hidden(true); + menu.on_cut() = [this]() { + show_nyi(); + }; + menu.on_paste() = [this]() { + show_nyi(); + }; + menu.on_copy() = [this]() { + show_nyi(); + }; + menu.on_delete_line() = [this]() { + show_nyi(); + }; + menu.on_edit_line() = [this]() { + show_nyi(); + }; + menu.on_add_line() = [this]() { + show_nyi(); + }; + menu.on_open() = [this]() { + // TODO: confirm. + show_file_picker(); + }; + menu.on_save() = [this]() { + show_nyi(); + }; + menu.on_exit() = [this]() { + // TODO: confirm. + nav_.pop(); + }; + + button_menu.on_select = [this]() { + if (file_) { + // Toggle menu. + hide_menu(!menu.hidden()); + } else { + show_file_picker(); + } + }; +} + +TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path) + : TextEditorView(nav) { + open_file(path); +} + +void TextEditorView::on_show() { + if (file_) + viewer.focus(); + else + button_menu.focus(); +} + +void TextEditorView::open_file(const fs::path& path) { + auto file = std::make_unique(); + auto error = file->open(path); + + if (error) { + nav_.display_modal("Read Error", "Cannot open file:\n" + error->what()); + file_.reset(); + viewer.clear_file(); + } else { + file_ = std::move(file); + viewer.set_file(*file_); + } + + refresh_ui(); +} + +void TextEditorView::refresh_ui() { + if (file_) { + update_position(); + text_size.set( + "Lines:" + to_string_dec_uint(file_->line_count()) + + " (" + to_string_file_size(file_->size()) + ")"); + } else { + text_position.set(""); + text_size.set(""); + } +} + +void TextEditorView::update_position() { + if (viewer.has_file()) { + text_position.set( + "Ln " + to_string_dec_uint(viewer.line() + 1) + + ", Col " + to_string_dec_uint(viewer.col() + 1)); + } +} + +void TextEditorView::hide_menu(bool hidden) { + menu.hidden(hidden); + + // Only let the viewer be focused when the menu is + // not shown, otherwise menu focus gets confusing. + viewer.set_focusable(hidden); + + if (hidden) + viewer.focus(); + + viewer.redraw(true); + set_dirty(); +} + +void TextEditorView::show_file_picker() { + auto open_view = nav_.push(""); + open_view->on_changed = [this](std::filesystem::path path) { + open_file(path); + hide_menu(); + }; +} + +void TextEditorView::show_nyi() { + nav_.display_modal("Soon...", "Coming soon."); } } // namespace ui \ No newline at end of file diff --git a/firmware/application/apps/ui_text_editor.hpp b/firmware/application/apps/ui_text_editor.hpp index 0c2c4b09..5a0cad46 100644 --- a/firmware/application/apps/ui_text_editor.hpp +++ b/firmware/application/apps/ui_text_editor.hpp @@ -27,17 +27,21 @@ #include "ui_navigation.hpp" #include "ui_painter.hpp" #include "ui_widget.hpp" -//#include "ui_textentry.hpp" #include "circular_buffer.hpp" #include "file.hpp" #include "optional.hpp" +#include #include #include namespace ui { +/* TODO: + * - Copy on write into temp file so startup is fast. + */ + enum class LineEnding : uint8_t { LF, CRLF @@ -80,6 +84,7 @@ class FileWrapper { private: /* Number of newline offsets to cache. */ static constexpr Offset max_newlines = 64; + static constexpr size_t buffer_size = 512; void initialize(); std::string read(Offset offset, Offset length = 30); @@ -107,6 +112,160 @@ class FileWrapper { CircularBuffer newlines_{}; }; +/* Control that renders a text file. */ +class TextViewer : public Widget { + public: + TextViewer(Rect parent_rect); + + TextViewer(const TextViewer&) = delete; + TextViewer(TextViewer&&) = delete; + TextViewer& operator=(const TextViewer&) = delete; + TextViewer& operator=(TextViewer&&) = delete; + + std::function on_select{}; + std::function on_cursor_moved{}; + + void paint(Painter& painter) override; + bool on_key(KeyEvent delta) override; + bool on_encoder(EncoderEvent delta) override; + + void redraw(bool redraw_text = false); + + void set_file(FileWrapper& file) { reset_file(&file); } + void clear_file() { reset_file(); } + bool has_file() const { return file_ != nullptr; } + + uint32_t line() const { return cursor_.line; } + uint32_t col() const { return cursor_.col; } + + private: + static constexpr int8_t char_width = 5; + static constexpr int8_t char_height = 8; + static constexpr Style style_text{ + .font = font::fixed_5x8, + .background = Color::black(), + .foreground = Color::white(), + }; + + const uint8_t max_line = 32; + const uint8_t max_col = 48; + + /* Returns true if the cursor was updated. */ + bool apply_scrolling_constraints( + int16_t delta_line, + int16_t delta_col); + + void paint_text(Painter& painter, uint32_t line, uint16_t col); + void paint_cursor(Painter& painter); + + void reset_file(FileWrapper* file = nullptr); + + // Gets the length of the current line. + uint16_t line_length(); + + FileWrapper* file_{}; + + struct { + // Previous cursor state. + uint32_t line{}; + uint16_t col{}; + + // Previous draw state. + uint32_t first_line{}; + uint16_t first_col{}; + bool redraw_text{true}; + } paint_state_{}; + + struct { + uint32_t line{}; + uint16_t col{}; + ScrollDirection dir{ScrollDirection::Vertical}; + } cursor_{}; +}; + +/* Menu control for the TextEditor. */ +class TextEditorMenu : public View { + public: + TextEditorMenu(); + + void on_show() override; + void on_hide() override; + + std::function& on_cut() { return button_cut.on_select; } + std::function& on_paste() { return button_paste.on_select; } + std::function& on_copy() { return button_copy.on_select; } + + std::function& on_delete_line() { return button_delline.on_select; } + std::function& on_edit_line() { return button_edit.on_select; } + std::function& on_add_line() { return button_addline.on_select; } + + std::function& on_open() { return button_open.on_select; } + std::function& on_save() { return button_save.on_select; } + std::function& on_exit() { return button_exit.on_select; } + + private: + void hide_children(bool hidden); + + Rectangle rect_frame{ + {0 * 8, 0 * 8, 23 * 8, 23 * 8}, + Color::dark_grey()}; + + NewButton button_cut{ + {1 * 8, 1 * 8, 7 * 8, 7 * 8}, + "Cut", + &bitmap_icon_cut, + Color::dark_grey()}; + + NewButton button_paste{ + {8 * 8, 1 * 8, 7 * 8, 7 * 8}, + "Paste", + &bitmap_icon_paste, + Color::dark_grey()}; + + NewButton button_copy{ + {15 * 8, 1 * 8, 7 * 8, 7 * 8}, + "Copy", + &bitmap_icon_copy, + Color::dark_grey()}; + + NewButton button_delline{ + {1 * 8, 8 * 8, 7 * 8, 7 * 8}, + "-Line", + &bitmap_icon_delete, + Color::dark_red()}; + + NewButton button_edit{ + {8 * 8, 8 * 8, 7 * 8, 7 * 8}, + "Edit", + &bitmap_icon_rename, + Color::dark_blue()}; + + NewButton button_addline{ + {15 * 8, 8 * 8, 7 * 8, 7 * 8}, + "+Line", + &bitmap_icon_scanner, + Color::dark_blue()}; + + NewButton button_open{ + {1 * 8, 15 * 8, 7 * 8, 7 * 8}, + "Open", + &bitmap_icon_load, + Color::green()}; + + NewButton button_save{ + {8 * 8, 15 * 8, 7 * 8, 7 * 8}, + "Save", + &bitmap_icon_save, + Color::green()}; + + NewButton button_exit{ + {15 * 8, 15 * 8, 7 * 8, 7 * 8}, + "Exit", + &bitmap_icon_previous, + Color::dark_red()}; +}; + +/* View viewing and minor edits on a text file. */ class TextEditorView : public View { public: TextEditorView(NavigationView& nav); @@ -118,72 +277,37 @@ class TextEditorView : public View { return "Notepad"; }; - void on_focus() override; - void paint(Painter& painter) override; - bool on_key(KeyEvent delta) override; - bool on_encoder(EncoderEvent delta) override; + void on_show() override; private: - static constexpr uint8_t max_line = 32; - static constexpr uint8_t max_col = 48; - static constexpr int8_t char_width = 5; - static constexpr int8_t char_height = 8; - - static constexpr Style style_default{ - .font = font::fixed_5x8, - .background = Color::black(), - .foreground = Color::white(), - }; - - /* Returns true if the cursor was updated. */ - bool apply_scrolling_constraints( - int16_t delta_line, - int16_t delta_col); - - void refresh_ui(); void open_file(const std::filesystem::path& path); - - void paint_text(Painter& painter, uint32_t line, uint16_t col); - void paint_cursor(Painter& painter); - - // Gets the length of the current line. - uint16_t line_length(); + void refresh_ui(); + void update_position(); + void hide_menu(bool hidden = true); + void show_file_picker(); + void show_nyi(); NavigationView& nav_; + std::unique_ptr file_{}; - FileWrapper file_{}; + TextViewer viewer{ + /* 272 = 320 - 16 (top bar) - 32 (bottom controls) */ + {0, 0, 240, 272}}; - struct { - // Previous cursor state. - uint32_t line{}; - uint16_t col{}; + TextEditorMenu menu{}; - // Previous draw state. - uint32_t first_line{}; - uint16_t first_col{}; - bool redraw_text{true}; - bool has_file{false}; - } paint_state_{}; - - struct { - uint32_t line{}; - uint16_t col{}; - ScrollDirection dir{ScrollDirection::Vertical}; - } cursor_{}; - - // TODO: The scrollable view should be its own widget - // otherwise control navigation doesn't work. - - Button button_open{ - {24 * 8, 34 * 8, 6 * 8, 4 * 8}, - "Open"}; + NewButton button_menu{ + {26 * 8, 34 * 8, 4 * 8, 4 * 8}, + {}, + &bitmap_icon_controls, + Color::dark_grey()}; Text text_position{ - {0 * 8, 34 * 8, 24 * 8, 2 * 8}, + {0 * 8, 34 * 8, 26 * 8, 2 * 8}, ""}; Text text_size{ - {0 * 8, 36 * 8, 24 * 8, 2 * 8}, + {0 * 8, 36 * 8, 26 * 8, 2 * 8}, ""}; }; diff --git a/firmware/application/bitmap.hpp b/firmware/application/bitmap.hpp index 22757bbc..8f5df2c7 100644 --- a/firmware/application/bitmap.hpp +++ b/firmware/application/bitmap.hpp @@ -2714,38 +2714,38 @@ static constexpr Bitmap bitmap_icon_remote{ bitmap_icon_remote_data}; static constexpr uint8_t bitmap_icon_save_data[] = { - 0x00, - 0x01, - 0x00, - 0x01, - 0x00, - 0x01, - 0x00, - 0x01, - 0x4E, - 0x05, - 0x91, - 0x03, - 0x3F, - 0x19, - 0x01, + 0xFC, + 0x07, + 0x0A, + 0x0A, + 0x0A, + 0x12, + 0xF2, + 0x21, + 0x02, 0x20, - 0xF9, - 0xFF, - 0xF9, - 0xFF, - 0xFD, - 0x7F, - 0xFD, - 0x7F, - 0xFF, - 0x3F, - 0xFF, - 0x3F, - 0xFF, - 0x1F, - 0xFF, + 0x02, + 0x20, + 0x02, + 0x20, + 0x02, + 0x20, + 0xFA, + 0x27, + 0xFA, + 0x2F, + 0x0A, + 0x28, + 0xFA, + 0x2F, + 0x0A, + 0x28, + 0xFA, + 0x2F, + 0xFC, 0x1F, + 0x00, + 0x00, }; static constexpr Bitmap bitmap_icon_save{ {16, 16}, diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index dc4ea85f..b051d72f 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -241,10 +241,11 @@ std::filesystem::filesystem_error rename_file( std::filesystem::filesystem_error copy_file( const std::filesystem::path& file_path, const std::filesystem::path& dest_path) { + // Decent compromise between memory and speed. + constexpr size_t buffer_size = 512; + uint8_t buffer[buffer_size]; File src; File dst; - constexpr size_t buffer_size = 128; - uint8_t buffer[buffer_size]; auto error = src.open(file_path); if (error.is_valid()) return error.value(); diff --git a/firmware/common/optional.hpp b/firmware/common/optional.hpp index 68712fc1..7ff4c6d8 100644 --- a/firmware/common/optional.hpp +++ b/firmware/common/optional.hpp @@ -35,6 +35,7 @@ class Optional { : value_{std::move(value)}, valid_{true} {} bool is_valid() const { return valid_; } + operator bool() const { return valid_; } // TODO: Throw if not valid? T& value() & { return value_; } diff --git a/firmware/graphics/icon_save.png b/firmware/graphics/icon_save.png index a01be34e8446c13211108496aa792397e9a09cf7..2efd786ff09cb167393663e283005be2769bb575 100644 GIT binary patch delta 171 zcmZ3@xQB6qWIYoD1H*)g8D>C=u{g-xiDBJ2nU_EgOS+@4BLl<6e(pbstUx|zfk$L9 z1A~|<2s3&HseAwm270=3(T;2(nMb;`Sp0xR>>}vMP6BFTmSJy3}`dH#y Tla;zK&~ye*S3j3^P6HY0jmL!8Gi-<001BJ|6u?C0B}h}K~y+T)sx!_z#t4m&HsONN5w!^D_#)B zd=ar8lSWY{!^{i-MrFed9{35sWl}QzQhJPGtiw@>*AS?wbP8s%5=6)g zppsWWUULs{bi4b|m{w(L o{1}; + REQUIRE((bool)o); +} + TEST_CASE("value() should return value.") { Optional o{1}; REQUIRE(o.value() == 1); } +TEST_CASE("operator* should return value.") { + Optional o{1}; + REQUIRE(*o == 1); +} + +TEST_CASE("operator-> should return value members.") { + struct S { + int i; + }; + + Optional o{S{1}}; + REQUIRE(o->i == 1); +} + TEST_SUITE_END(); \ No newline at end of file