mirror of
https://github.com/eried/portapack-mayhem.git
synced 2024-10-01 01:26:06 -04:00
parent
53fcdedb88
commit
00667cecf9
@ -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 <typename T>
|
||||
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::Error> 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::Range> 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::Range> 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::Offset> 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::Offset> FileWrapper::previous_newline(Offset start) {
|
||||
}
|
||||
|
||||
Optional<FileWrapper::Offset> FileWrapper::next_newline(Offset start) {
|
||||
constexpr size_t buffer_size = 128;
|
||||
char buffer[buffer_size];
|
||||
Offset offset = start;
|
||||
|
||||
@ -255,41 +249,20 @@ Optional<FileWrapper::Offset> 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<uint8_t>(parent_rect.height() / char_height)},
|
||||
max_col{static_cast<uint8_t>(parent_rect.width() / char_width)} {
|
||||
set_focusable(true);
|
||||
|
||||
button_open.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>("");
|
||||
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<FileWrapper>();
|
||||
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<FileLoadView>("");
|
||||
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
|
@ -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 <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<Offset, max_newlines + 1> 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<void()> on_select{};
|
||||
std::function<void()> 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<void()>& on_cut() { return button_cut.on_select; }
|
||||
std::function<void()>& on_paste() { return button_paste.on_select; }
|
||||
std::function<void()>& on_copy() { return button_copy.on_select; }
|
||||
|
||||
std::function<void()>& on_delete_line() { return button_delline.on_select; }
|
||||
std::function<void()>& on_edit_line() { return button_edit.on_select; }
|
||||
std::function<void()>& on_add_line() { return button_addline.on_select; }
|
||||
|
||||
std::function<void()>& on_open() { return button_open.on_select; }
|
||||
std::function<void()>& on_save() { return button_save.on_select; }
|
||||
std::function<void()>& 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<FileWrapper> 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},
|
||||
""};
|
||||
};
|
||||
|
||||
|
@ -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},
|
||||
|
@ -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();
|
||||
|
@ -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_; }
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 171 B After Width: | Height: | Size: 188 B |
@ -34,9 +34,28 @@ TEST_CASE("Instance with value should be valid.") {
|
||||
REQUIRE(o.is_valid());
|
||||
}
|
||||
|
||||
TEST_CASE("Instance with value should be return true.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE((bool)o);
|
||||
}
|
||||
|
||||
TEST_CASE("value() should return value.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE(o.value() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("operator* should return value.") {
|
||||
Optional<int> o{1};
|
||||
REQUIRE(*o == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("operator-> should return value members.") {
|
||||
struct S {
|
||||
int i;
|
||||
};
|
||||
|
||||
Optional<S> o{S{1}};
|
||||
REQUIRE(o->i == 1);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
Loading…
Reference in New Issue
Block a user