mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2025-02-22 22:48:28 +00:00
Lazy line caching for Notepad (#1042)
* easier 'now', start adding text editor * Adding scrolling to notepad * Better scrolling * Better scrolling, off-by-1 bugs * MVP fit and finish * Add tiny font and use in Notepad * Font tweaking, tiny font cursor * Fix warning * Format changed files * WIP No file limit * WIP - adding CircularBuffer type * WIP Caching * add unit test for circular_buffer * WIP still have a bug when moving cache forward * Finish lazy line caching --------- Co-authored-by: kallanreed <kallanreed@outlook.com>
This commit is contained in:
parent
802a4e243b
commit
98f3bf151f
@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
#include "ui_fileman.hpp"
|
#include "ui_fileman.hpp"
|
||||||
#include "ui_text_editor.hpp"
|
#include "ui_text_editor.hpp"
|
||||||
|
|
||||||
|
#include "log_file.hpp"
|
||||||
#include "string_format.hpp"
|
#include "string_format.hpp"
|
||||||
|
|
||||||
using namespace portapack;
|
using namespace portapack;
|
||||||
@ -31,19 +33,240 @@ template <typename T>
|
|||||||
T mid(const T& val1, const T& val2, const T& val3) {
|
T mid(const T& val1, const T& val2, const T& val3) {
|
||||||
return std::max(val1, std::min(val2, val3));
|
return std::max(val1, std::min(val2, val3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*void log(const std::string& msg) {
|
||||||
|
LogFile log{};
|
||||||
|
log.append("LOGS/Notepad.txt");
|
||||||
|
log.write_entry(msg);
|
||||||
|
}*/
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
|
/* FileWrapper ******************************************************/
|
||||||
|
|
||||||
|
FileWrapper::FileWrapper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<FileWrapper::Error> FileWrapper::open(const fs::path& path) {
|
||||||
|
file_ = File();
|
||||||
|
auto result = file_.open(path);
|
||||||
|
|
||||||
|
if (!result.is_valid()) // No error
|
||||||
|
initialize();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FileWrapper::get_text(Offset line, Offset col, Offset length) {
|
||||||
|
// TODO: better way to return errors.
|
||||||
|
auto range = line_range(line);
|
||||||
|
int32_t to_read = length;
|
||||||
|
|
||||||
|
if (!range.is_valid())
|
||||||
|
return "[UNCACHED LINE]";
|
||||||
|
|
||||||
|
// Don't read past end of line.
|
||||||
|
if (range->start + col + to_read >= range->end)
|
||||||
|
to_read = range->end - col - range->start;
|
||||||
|
|
||||||
|
if (to_read <= 0)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return read(range->start + col, to_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<FileWrapper::Range> FileWrapper::line_range(Line line) {
|
||||||
|
ensure_cached(line);
|
||||||
|
|
||||||
|
auto offset = offset_for_line(line);
|
||||||
|
if (!offset.is_valid())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1);
|
||||||
|
auto end = newlines_[*offset] + 1;
|
||||||
|
|
||||||
|
return {Range{start, end}};
|
||||||
|
}
|
||||||
|
|
||||||
|
FileWrapper::Offset FileWrapper::line_length(Line line) {
|
||||||
|
auto range = line_range(line);
|
||||||
|
|
||||||
|
if (range.is_valid())
|
||||||
|
return range->end - range->start;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileWrapper::initialize() {
|
||||||
|
start_offset_ = 0;
|
||||||
|
start_line_ = 0;
|
||||||
|
line_count_ = 0;
|
||||||
|
newlines_.clear();
|
||||||
|
line_ending_ = LineEnding::LF;
|
||||||
|
|
||||||
|
Offset offset = 0;
|
||||||
|
auto result = next_newline(offset);
|
||||||
|
|
||||||
|
while (result.is_valid()) {
|
||||||
|
++line_count_;
|
||||||
|
if (newlines_.size() < max_newlines)
|
||||||
|
newlines_.push_back(*result);
|
||||||
|
offset = *result + 1;
|
||||||
|
result = next_newline(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FileWrapper::read(Offset offset, Offset length) {
|
||||||
|
// TODO: better way to return errors.
|
||||||
|
if (offset + length > file_.size())
|
||||||
|
return {"[BAD OFFSET]"};
|
||||||
|
|
||||||
|
std::string buffer(length, '\0');
|
||||||
|
file_.seek(offset);
|
||||||
|
|
||||||
|
auto result = file_.read(&buffer[0], length);
|
||||||
|
if (result.is_ok())
|
||||||
|
buffer.resize(*result);
|
||||||
|
else
|
||||||
|
return result.error().what();
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<FileWrapper::Offset> FileWrapper::offset_for_line(Line line) const {
|
||||||
|
if (line >= line_count_)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Offset actual = line - start_line_;
|
||||||
|
if (actual < newlines_.size()) // NB: underflow wrap.
|
||||||
|
return {actual};
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileWrapper::ensure_cached(Line line) {
|
||||||
|
if (line >= line_count_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto result = offset_for_line(line);
|
||||||
|
if (result.is_valid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (line < start_line_) {
|
||||||
|
while (line < start_line_ && start_offset_ >= 2) {
|
||||||
|
// start_offset_ - 1 should be a newline. Need to
|
||||||
|
// find the new value for start_offset_. start_line_
|
||||||
|
// has to be > 0 to get into this block so there should
|
||||||
|
// always be one newline before start_offset_.
|
||||||
|
auto offset = previous_newline(start_offset_ - 2);
|
||||||
|
newlines_.push_front(start_offset_ - 1);
|
||||||
|
|
||||||
|
if (!offset.is_valid()) {
|
||||||
|
// Must be at beginning.
|
||||||
|
start_line_ = 0;
|
||||||
|
start_offset_ = 0;
|
||||||
|
} else {
|
||||||
|
// Found an previous newline, the new start_line_
|
||||||
|
// starts at the newline offset + 1.
|
||||||
|
start_line_--;
|
||||||
|
start_offset_ = *offset + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (line >= start_line_ + newlines_.size()) {
|
||||||
|
auto offset = next_newline(newlines_.back() + 1);
|
||||||
|
if (offset.is_valid()) {
|
||||||
|
start_line_++;
|
||||||
|
start_offset_ = newlines_.front() + 1;
|
||||||
|
newlines_.push_back(*offset);
|
||||||
|
} /* else at the EOF. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (offset < to_read) {
|
||||||
|
// NB: Char at 'offset' was read in the previous iteration.
|
||||||
|
to_read = offset;
|
||||||
|
offset = 0;
|
||||||
|
} else
|
||||||
|
offset -= to_read;
|
||||||
|
|
||||||
|
file_.seek(offset);
|
||||||
|
|
||||||
|
auto result = file_.read(buffer, to_read);
|
||||||
|
if (result.is_error())
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Find newlines in the buffer backwards.
|
||||||
|
for (int32_t i = *result - 1; i >= 0; --i) {
|
||||||
|
switch (buffer[i]) {
|
||||||
|
case '\n':
|
||||||
|
return {offset + i};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
} while (true);
|
||||||
|
|
||||||
|
return {}; // Didn't find one.
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<FileWrapper::Offset> FileWrapper::next_newline(Offset start) {
|
||||||
|
constexpr size_t buffer_size = 128;
|
||||||
|
char buffer[buffer_size];
|
||||||
|
Offset offset = start;
|
||||||
|
|
||||||
|
// EOF, nothing to do.
|
||||||
|
if (start >= size())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
file_.seek(offset);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
auto result = file_.read(buffer, buffer_size);
|
||||||
|
if (result.is_error())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
// Find newlines in the buffer.
|
||||||
|
for (Offset i = 0; i < *result; ++i) {
|
||||||
|
switch (buffer[i]) {
|
||||||
|
case '\n':
|
||||||
|
return {offset + i};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += *result;
|
||||||
|
|
||||||
|
if (*result < buffer_size)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fake a newline at the end for consistency.
|
||||||
|
return {offset};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TextEditorView ***************************************************/
|
||||||
|
|
||||||
TextEditorView::TextEditorView(NavigationView& nav)
|
TextEditorView::TextEditorView(NavigationView& nav)
|
||||||
: nav_{nav} {
|
: nav_{nav} {
|
||||||
add_children(
|
add_children(
|
||||||
{&button_open,
|
{
|
||||||
&text_position});
|
&button_open,
|
||||||
|
&text_position,
|
||||||
|
&text_size,
|
||||||
|
});
|
||||||
set_focusable(true);
|
set_focusable(true);
|
||||||
|
|
||||||
// log_.append("LOGS/NOTEPAD.TXT");
|
|
||||||
|
|
||||||
button_open.on_select = [this](Button&) {
|
button_open.on_select = [this](Button&) {
|
||||||
auto open_view = nav_.push<FileLoadView>(".TXT");
|
auto open_view = nav_.push<FileLoadView>(".TXT");
|
||||||
open_view->on_changed = [this](std::filesystem::path path) {
|
open_view->on_changed = [this](std::filesystem::path path) {
|
||||||
@ -54,6 +277,7 @@ TextEditorView::TextEditorView(NavigationView& nav)
|
|||||||
|
|
||||||
void TextEditorView::on_focus() {
|
void TextEditorView::on_focus() {
|
||||||
refresh_ui();
|
refresh_ui();
|
||||||
|
button_open.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextEditorView::paint(Painter& painter) {
|
void TextEditorView::paint(Painter& painter) {
|
||||||
@ -63,16 +287,19 @@ void TextEditorView::paint(Painter& painter) {
|
|||||||
if (!paint_state_.has_file)
|
if (!paint_state_.has_file)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Move the viewport vertically.
|
||||||
if (cursor_.line < first_line)
|
if (cursor_.line < first_line)
|
||||||
first_line = cursor_.line;
|
first_line = cursor_.line;
|
||||||
else if (cursor_.line >= first_line + max_line)
|
else if (cursor_.line >= first_line + max_line)
|
||||||
first_line = cursor_.line - max_line + 1;
|
first_line = cursor_.line - max_line + 1;
|
||||||
|
|
||||||
|
// Move the viewport horizontally.
|
||||||
if (cursor_.col < first_col)
|
if (cursor_.col < first_col)
|
||||||
first_col = cursor_.col;
|
first_col = cursor_.col;
|
||||||
if (cursor_.col >= first_col + max_col)
|
if (cursor_.col >= first_col + max_col)
|
||||||
first_col = cursor_.col - max_col + 1;
|
first_col = cursor_.col - max_col + 1;
|
||||||
|
|
||||||
|
// Viewport updated? Redraw text.
|
||||||
if (first_line != paint_state_.first_line ||
|
if (first_line != paint_state_.first_line ||
|
||||||
first_col != paint_state_.first_col) {
|
first_col != paint_state_.first_col) {
|
||||||
paint_state_.first_line = first_line;
|
paint_state_.first_line = first_line;
|
||||||
@ -129,28 +356,27 @@ bool TextEditorView::on_encoder(EncoderEvent delta) {
|
|||||||
bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) {
|
bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) {
|
||||||
int32_t new_line = cursor_.line + delta_line;
|
int32_t new_line = cursor_.line + delta_line;
|
||||||
int32_t new_col = cursor_.col + delta_col;
|
int32_t new_col = cursor_.col + delta_col;
|
||||||
|
int32_t new_line_length = file_.line_length(new_line);
|
||||||
auto new_line_length = info_.line_length(new_line);
|
|
||||||
|
|
||||||
if (new_col < 0)
|
if (new_col < 0)
|
||||||
--new_line;
|
--new_line;
|
||||||
else if (new_col > new_line_length && delta_line == 0) {
|
else if (new_col >= new_line_length && delta_line == 0) {
|
||||||
// Only want to wrap if moving horizontally.
|
// Only wrap if moving horizontally.
|
||||||
new_col = 0;
|
new_col = 0;
|
||||||
++new_line;
|
++new_line;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new_line < 0 || (uint32_t)new_line >= info_.line_count())
|
if (new_line < 0 || (uint32_t)new_line >= file_.line_count())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
new_line_length = info_.line_length(new_line);
|
new_line_length = file_.line_length(new_line);
|
||||||
|
|
||||||
// TODO: don't wrap with encoder?
|
// TODO: don't wrap with encoder?
|
||||||
// Wrap or clamp column.
|
// Wrap or clamp column.
|
||||||
if (new_line_length == 0)
|
if (new_line_length == 0)
|
||||||
new_col = 0;
|
new_col = 0;
|
||||||
else if (new_col > new_line_length || new_col < 0)
|
else if (new_col >= new_line_length || new_col < 0)
|
||||||
new_col = new_line_length;
|
new_col = new_line_length - 1;
|
||||||
|
|
||||||
cursor_.line = new_line;
|
cursor_.line = new_line;
|
||||||
cursor_.col = new_col;
|
cursor_.col = new_col;
|
||||||
@ -161,134 +387,65 @@ bool TextEditorView::apply_scrolling_constraints(int16_t delta_line, int16_t del
|
|||||||
void TextEditorView::refresh_ui() {
|
void TextEditorView::refresh_ui() {
|
||||||
if (paint_state_.has_file) {
|
if (paint_state_.has_file) {
|
||||||
text_position.set(
|
text_position.set(
|
||||||
to_string_dec_uint(cursor_.col + 1) + ":" +
|
"Ln " + to_string_dec_uint(cursor_.line + 1) +
|
||||||
to_string_dec_uint(cursor_.line + 1) + "/" +
|
", Col " + to_string_dec_uint(cursor_.col + 1));
|
||||||
to_string_dec_uint(info_.line_count()) +
|
text_size.set(
|
||||||
(info_.truncated ? "*" : "") +
|
"Lines:" + to_string_dec_uint(file_.line_count()) +
|
||||||
" Size: " +
|
" (" + to_string_file_size(file_.size()) + ")");
|
||||||
to_string_file_size(info_.size));
|
|
||||||
focus();
|
|
||||||
} else {
|
} else {
|
||||||
button_open.focus();
|
// if (!button_open.has_focus())
|
||||||
|
// button_open.focus();
|
||||||
|
text_position.set("");
|
||||||
|
text_size.set("");
|
||||||
}
|
}
|
||||||
|
|
||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextEditorView::refresh_file_info() {
|
|
||||||
constexpr size_t buffer_size = 128;
|
|
||||||
char buffer[buffer_size];
|
|
||||||
uint32_t base_offset = 0;
|
|
||||||
|
|
||||||
file_.seek(0);
|
|
||||||
info_.newlines.clear();
|
|
||||||
info_.line_ending = LineEnding::LF;
|
|
||||||
info_.size = file_.size();
|
|
||||||
info_.truncated = false;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
auto result = file_.read(buffer, buffer_size);
|
|
||||||
if (result.is_error())
|
|
||||||
break; // TODO: report error?
|
|
||||||
|
|
||||||
// TODO: CRLF state machine for cross block.
|
|
||||||
for (uint32_t i = 0; i < result.value(); ++i) {
|
|
||||||
switch (buffer[i]) {
|
|
||||||
case '\n':
|
|
||||||
info_.newlines.push_back(base_offset + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
base_offset += result.value();
|
|
||||||
|
|
||||||
// Fake a newline at the end for consistency.
|
|
||||||
// Could check if there already is a trailing newline, but it doesn't hurt.
|
|
||||||
if (result.value() < buffer_size) {
|
|
||||||
info_.newlines.push_back(base_offset);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HACK HACK: only show first 1000 lines for now.
|
|
||||||
if (info_.newlines.size() >= 1000) {
|
|
||||||
info_.truncated = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh_ui();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextEditorView::open_file(const fs::path& path) {
|
void TextEditorView::open_file(const fs::path& path) {
|
||||||
// TODO: need a temp backing file for edits.
|
// TODO: need a temp backing file for edits.
|
||||||
|
|
||||||
auto result = file_.open(path);
|
auto result = file_.open(path);
|
||||||
paint_state_.has_file = !result.is_valid(); /* Has an error. */
|
|
||||||
|
|
||||||
if (paint_state_.has_file) {
|
if (result.is_valid())
|
||||||
refresh_file_info();
|
nav_.display_modal("Read Error", "Cannot open file:\n" + result->what());
|
||||||
paint_state_.first_line = 0;
|
|
||||||
paint_state_.first_col = 0;
|
paint_state_.has_file = !result.is_valid(); // Has an error.
|
||||||
cursor_.line = 0;
|
paint_state_.first_line = 0;
|
||||||
cursor_.col = 0;
|
paint_state_.first_col = 0;
|
||||||
} else {
|
cursor_.line = 0;
|
||||||
nav_.display_modal("Read Error", "Cannot open file:\n" + result.value().what());
|
cursor_.col = 0;
|
||||||
paint_state_.has_file = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
paint_state_.redraw_text = true;
|
paint_state_.redraw_text = true;
|
||||||
refresh_ui();
|
refresh_ui();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string TextEditorView::read(uint32_t offset, uint32_t length) {
|
|
||||||
if (offset >= info_.size)
|
|
||||||
return {"[BAD OFFSET]"};
|
|
||||||
|
|
||||||
std::string buffer(length + 1, '\0');
|
|
||||||
file_.seek(offset);
|
|
||||||
|
|
||||||
auto result = file_.read(&buffer[0], length);
|
|
||||||
if (result.is_ok())
|
|
||||||
/* resize? */;
|
|
||||||
else
|
|
||||||
return result.error().what();
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextEditorView::paint_text(Painter& painter, uint32_t line, uint16_t col) {
|
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.
|
// TODO: A line cache would use more memory but save a lot of IO.
|
||||||
// Only the new lines/characters would need to be refetched.
|
// Only the new lines/characters would need to be refetched.
|
||||||
|
|
||||||
auto r = screen_rect();
|
auto r = screen_rect();
|
||||||
auto& lines = info_.newlines;
|
|
||||||
auto line_start = info_.line_start(line);
|
|
||||||
|
|
||||||
// Draw the lines from the file
|
// Draw the lines from the file
|
||||||
for (uint32_t i = 0; i < max_line && i < lines.size(); ++i) {
|
for (auto i = 0u; i < max_line; ++i) {
|
||||||
auto line_end = lines[line + i];
|
if (line + i >= file_.line_count())
|
||||||
int32_t read_length = max_col;
|
break;
|
||||||
|
|
||||||
// Don't read past end of the line.
|
auto str = file_.get_text(line + i, col, max_col);
|
||||||
if (line_start + col + (uint32_t)read_length > line_end)
|
|
||||||
read_length = line_end - col - line_start;
|
|
||||||
|
|
||||||
if (read_length > 0)
|
// Draw text.
|
||||||
|
if (str.length() > 0)
|
||||||
painter.draw_string(
|
painter.draw_string(
|
||||||
{0, r.location().y() + (int)i * char_height},
|
{0, r.location().y() + (int)i * char_height},
|
||||||
style_default, read(line_start + col, read_length));
|
style_default, str);
|
||||||
|
|
||||||
// Erase empty line sectons.
|
// Clear empty line sections.
|
||||||
if (read_length >= 0) {
|
int32_t clear_width = max_col - str.length();
|
||||||
int32_t clear_width = max_col - read_length;
|
if (clear_width > 0)
|
||||||
if (clear_width > 0)
|
painter.fill_rectangle(
|
||||||
painter.fill_rectangle(
|
{(max_col - clear_width) * char_width,
|
||||||
{(max_col - clear_width) * char_width,
|
r.location().y() + (int)i * char_height,
|
||||||
r.location().y() + (int)i * char_height,
|
clear_width * char_width, char_height},
|
||||||
clear_width * char_width, char_height},
|
style_default.background);
|
||||||
style_default.background);
|
|
||||||
}
|
|
||||||
|
|
||||||
line_start = lines[line + i] + 1 /* newline */;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,9 +462,7 @@ void TextEditorView::paint_cursor(Painter& painter) {
|
|||||||
c);
|
c);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TOOD: bug where cursor doesn't clear at EOF.
|
|
||||||
// TODO: XOR cursor?
|
// TODO: XOR cursor?
|
||||||
|
|
||||||
// Clear old cursor.
|
// Clear old cursor.
|
||||||
draw_cursor(paint_state_.line, paint_state_.col, style_default.background);
|
draw_cursor(paint_state_.line, paint_state_.col, style_default.background);
|
||||||
draw_cursor(cursor_.line, cursor_.col, style_default.foreground);
|
draw_cursor(cursor_.line, cursor_.col, style_default.foreground);
|
||||||
@ -315,8 +470,8 @@ void TextEditorView::paint_cursor(Painter& painter) {
|
|||||||
paint_state_.col = cursor_.col;
|
paint_state_.col = cursor_.col;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t TextEditorView::line_length() const {
|
uint16_t TextEditorView::line_length() {
|
||||||
return info_.line_length(cursor_.line);
|
return file_.line_length(cursor_.line);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
@ -29,8 +29,9 @@
|
|||||||
#include "ui_widget.hpp"
|
#include "ui_widget.hpp"
|
||||||
//#include "ui_textentry.hpp"
|
//#include "ui_textentry.hpp"
|
||||||
|
|
||||||
|
#include "circular_buffer.hpp"
|
||||||
#include "file.hpp"
|
#include "file.hpp"
|
||||||
#include "log_file.hpp"
|
#include "optional.hpp"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@ -47,44 +48,68 @@ enum class ScrollDirection : uint8_t {
|
|||||||
Horizontal
|
Horizontal
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: RAM is _very_ limited. Need to
|
/* Wraps a file and provides an API for accessing lines efficiently. */
|
||||||
// rework this to not store every line.
|
class FileWrapper {
|
||||||
// Should be able to get away with only
|
public:
|
||||||
// abount one screen of lines so long as
|
using Error = std::filesystem::filesystem_error;
|
||||||
// you can't scroll more than one screen
|
using Offset = uint32_t; // TODO: make enums?
|
||||||
// at a time.
|
using Line = uint32_t;
|
||||||
struct FileInfo {
|
using Column = uint32_t;
|
||||||
/* Offsets of newlines. */
|
using Range = struct {
|
||||||
std::vector<uint32_t> newlines;
|
// Offset of the line start.
|
||||||
LineEnding line_ending;
|
Offset start;
|
||||||
File::Size size;
|
// Offset of one past the line end.
|
||||||
bool truncated;
|
Offset end;
|
||||||
|
};
|
||||||
|
|
||||||
uint32_t line_count() const {
|
FileWrapper();
|
||||||
return newlines.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t line_start(uint32_t line) const {
|
/* Prevent copies. */
|
||||||
return line == 0 ? 0 : (newlines[line - 1] + 1);
|
FileWrapper(const FileWrapper&) = delete;
|
||||||
}
|
FileWrapper& operator=(const FileWrapper&) = delete;
|
||||||
|
|
||||||
uint16_t line_length(uint32_t line) const {
|
Optional<Error> open(const std::filesystem::path& path);
|
||||||
if (line >= line_count())
|
std::string get_text(Line line, Column col, Offset length);
|
||||||
return 0;
|
|
||||||
|
|
||||||
auto start = line_start(line);
|
File::Size size() const { return file_.size(); }
|
||||||
auto end = newlines[line];
|
uint32_t line_count() const { return line_count_; }
|
||||||
return end - start;
|
|
||||||
}
|
Optional<Range> line_range(Line line);
|
||||||
|
Offset line_length(Line line);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/* Number of newline offsets to cache. */
|
||||||
|
static constexpr Offset max_newlines = 64;
|
||||||
|
|
||||||
|
void initialize();
|
||||||
|
std::string read(Offset offset, Offset length = 30);
|
||||||
|
|
||||||
|
/* Returns the offset into the newline cache if valid. */
|
||||||
|
Optional<Offset> offset_for_line(Line line) const;
|
||||||
|
|
||||||
|
/* Ensure specified line is in the newline cache. */
|
||||||
|
void ensure_cached(Line line);
|
||||||
|
|
||||||
|
/* Helpers for finding the prev/next newline. */
|
||||||
|
Optional<Offset> previous_newline(Offset start);
|
||||||
|
Optional<Offset> next_newline(Offset start);
|
||||||
|
|
||||||
|
File file_{};
|
||||||
|
|
||||||
|
/* Total number of lines in the file. */
|
||||||
|
Offset line_count_{0};
|
||||||
|
|
||||||
|
/* The offset and line of the newlines cache. */
|
||||||
|
Offset start_offset_{0};
|
||||||
|
Offset start_line_{0};
|
||||||
|
|
||||||
|
LineEnding line_ending_{LineEnding::LF};
|
||||||
|
CircularBuffer<Offset, max_newlines + 1> newlines_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
/*class TextViewer : public Widget {
|
|
||||||
};*/
|
|
||||||
|
|
||||||
class TextEditorView : public View {
|
class TextEditorView : public View {
|
||||||
public:
|
public:
|
||||||
TextEditorView(NavigationView& nav);
|
TextEditorView(NavigationView& nav);
|
||||||
// TextEditorView(NavigationView& nav, const std::filesystem::path& path);
|
|
||||||
|
|
||||||
std::string title() const override {
|
std::string title() const override {
|
||||||
return "Notepad";
|
return "Notepad";
|
||||||
@ -101,7 +126,6 @@ class TextEditorView : public View {
|
|||||||
static constexpr int8_t char_width = 5;
|
static constexpr int8_t char_width = 5;
|
||||||
static constexpr int8_t char_height = 8;
|
static constexpr int8_t char_height = 8;
|
||||||
|
|
||||||
// TODO: should these be common somewhere?
|
|
||||||
static constexpr Style style_default{
|
static constexpr Style style_default{
|
||||||
.font = font::fixed_5x8,
|
.font = font::fixed_5x8,
|
||||||
.background = Color::black(),
|
.background = Color::black(),
|
||||||
@ -114,21 +138,17 @@ class TextEditorView : public View {
|
|||||||
int16_t delta_col);
|
int16_t delta_col);
|
||||||
|
|
||||||
void refresh_ui();
|
void refresh_ui();
|
||||||
void refresh_file_info();
|
|
||||||
void open_file(const std::filesystem::path& path);
|
void open_file(const std::filesystem::path& path);
|
||||||
std::string read(uint32_t offset, uint32_t length = 30);
|
|
||||||
|
|
||||||
void paint_text(Painter& painter, uint32_t line, uint16_t col);
|
void paint_text(Painter& painter, uint32_t line, uint16_t col);
|
||||||
void paint_cursor(Painter& painter);
|
void paint_cursor(Painter& painter);
|
||||||
|
|
||||||
// Gets the length of the current line.
|
// Gets the length of the current line.
|
||||||
uint16_t line_length() const;
|
uint16_t line_length();
|
||||||
|
|
||||||
NavigationView& nav_;
|
NavigationView& nav_;
|
||||||
|
|
||||||
File file_{};
|
FileWrapper file_{};
|
||||||
FileInfo info_{};
|
|
||||||
// LogFile log_{ };
|
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
// Previous cursor state.
|
// Previous cursor state.
|
||||||
@ -148,11 +168,6 @@ class TextEditorView : public View {
|
|||||||
ScrollDirection dir{ScrollDirection::Vertical};
|
ScrollDirection dir{ScrollDirection::Vertical};
|
||||||
} cursor_{};
|
} cursor_{};
|
||||||
|
|
||||||
/* 8px grid is 30 wide, 38 tall. */
|
|
||||||
/* 16px font height or 19 rows. */
|
|
||||||
/* Titlebar is 16px tall, so 18 rows left. */
|
|
||||||
/* 240 x 320, (304 with titlebar) */
|
|
||||||
|
|
||||||
// TODO: The scrollable view should be its own widget
|
// TODO: The scrollable view should be its own widget
|
||||||
// otherwise control navigation doesn't work.
|
// otherwise control navigation doesn't work.
|
||||||
|
|
||||||
@ -161,6 +176,10 @@ class TextEditorView : public View {
|
|||||||
"Open"};
|
"Open"};
|
||||||
|
|
||||||
Text text_position{
|
Text text_position{
|
||||||
|
{0 * 8, 34 * 8, 24 * 8, 2 * 8},
|
||||||
|
""};
|
||||||
|
|
||||||
|
Text text_size{
|
||||||
{0 * 8, 36 * 8, 24 * 8, 2 * 8},
|
{0 * 8, 36 * 8, 24 * 8, 2 * 8},
|
||||||
""};
|
""};
|
||||||
};
|
};
|
||||||
|
@ -97,8 +97,8 @@ File::Result<File::Offset> File::seek(const Offset new_position) {
|
|||||||
return {static_cast<File::Offset>(old_position)};
|
return {static_cast<File::Offset>(old_position)};
|
||||||
}
|
}
|
||||||
|
|
||||||
File::Size File::size() {
|
File::Size File::size() const {
|
||||||
return {static_cast<File::Size>(f_size(&f))};
|
return f_size(&f);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<File::Error> File::write_line(const std::string& s) {
|
Optional<File::Error> File::write_line(const std::string& s) {
|
||||||
|
@ -311,6 +311,10 @@ class File {
|
|||||||
return value_;
|
return value_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const T& operator*() const& {
|
||||||
|
return value_;
|
||||||
|
}
|
||||||
|
|
||||||
Error error() const {
|
Error error() const {
|
||||||
return error_;
|
return error_;
|
||||||
}
|
}
|
||||||
@ -339,6 +343,9 @@ class File {
|
|||||||
File(){};
|
File(){};
|
||||||
~File();
|
~File();
|
||||||
|
|
||||||
|
File(File&&) = default;
|
||||||
|
File& operator=(File&&) = default;
|
||||||
|
|
||||||
/* Prevent copies */
|
/* Prevent copies */
|
||||||
File(const File&) = delete;
|
File(const File&) = delete;
|
||||||
File& operator=(const File&) = delete;
|
File& operator=(const File&) = delete;
|
||||||
@ -352,8 +359,8 @@ class File {
|
|||||||
Result<Size> write(const void* const data, const Size bytes_to_write);
|
Result<Size> write(const void* const data, const Size bytes_to_write);
|
||||||
|
|
||||||
Result<Offset> seek(const uint64_t Offset);
|
Result<Offset> seek(const uint64_t Offset);
|
||||||
Timestamp created_date();
|
// Timestamp created_date() const;
|
||||||
Size size();
|
Size size() const;
|
||||||
|
|
||||||
template <size_t N>
|
template <size_t N>
|
||||||
Result<Size> write(const std::array<uint8_t, N>& data) {
|
Result<Size> write(const std::array<uint8_t, N>& data) {
|
||||||
|
@ -315,7 +315,6 @@ bool save_freqman_file(std::string& file_stem, freqman_db& db) {
|
|||||||
auto& entry = db[n];
|
auto& entry = db[n];
|
||||||
get_freq_string(entry, item_string);
|
get_freq_string(entry, item_string);
|
||||||
freqman_file.write_line(item_string);
|
freqman_file.write_line(item_string);
|
||||||
delete &item_string;
|
|
||||||
}
|
}
|
||||||
delete_file(freq_file_path);
|
delete_file(freq_file_path);
|
||||||
rename_file(tmp_freq_file_path, freq_file_path);
|
rename_file(tmp_freq_file_path, freq_file_path);
|
||||||
|
@ -338,11 +338,11 @@ const uint8_t fixed_5x8_glyph_data[] = {
|
|||||||
0x02,
|
0x02,
|
||||||
|
|
||||||
// Index: 44 (0x2C) Char: 0x004C ('L')
|
// Index: 44 (0x2C) Char: 0x004C ('L')
|
||||||
0x20,
|
0x40,
|
||||||
|
0x08,
|
||||||
|
0x21,
|
||||||
0x84,
|
0x84,
|
||||||
0x10,
|
0x03,
|
||||||
0xC2,
|
|
||||||
0x01,
|
|
||||||
|
|
||||||
// Index: 45 (0x2D) Char: 0x004D ('M')
|
// Index: 45 (0x2D) Char: 0x004D ('M')
|
||||||
0xA0,
|
0xA0,
|
||||||
@ -654,8 +654,8 @@ const uint8_t fixed_5x8_glyph_data[] = {
|
|||||||
|
|
||||||
// Index: 89 (0x59) Char: 0x0079 ('y')
|
// Index: 89 (0x59) Char: 0x0079 ('y')
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x80,
|
||||||
0xE5,
|
0xE4,
|
||||||
0x90,
|
0x90,
|
||||||
0x01,
|
0x01,
|
||||||
|
|
||||||
|
120
firmware/common/circular_buffer.hpp
Normal file
120
firmware/common/circular_buffer.hpp
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* 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 __CIRCULAR_BUFFER_H__
|
||||||
|
#define __CIRCULAR_BUFFER_H__
|
||||||
|
|
||||||
|
#include <stddef.h> // For size_t
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
/* Implements a fixed-size, circular buffer.
|
||||||
|
* NB: Holds Capacity - 1 items.
|
||||||
|
* There are no bounds checks on accessors so ensure there are
|
||||||
|
* items in the buffer before accessing front/back/operator[]. */
|
||||||
|
template <typename T, size_t Capacity>
|
||||||
|
class CircularBuffer {
|
||||||
|
public:
|
||||||
|
CircularBuffer() = default;
|
||||||
|
|
||||||
|
CircularBuffer(const CircularBuffer&) = delete;
|
||||||
|
CircularBuffer(CircularBuffer&&) = delete;
|
||||||
|
CircularBuffer& operator=(const CircularBuffer&) = delete;
|
||||||
|
CircularBuffer& operator=(CircularBuffer&&) = delete;
|
||||||
|
|
||||||
|
void push_front(T val) {
|
||||||
|
head_ = head_ > 0 ? head_ - 1 : last_index;
|
||||||
|
if (head_ == end_)
|
||||||
|
pop_back_internal();
|
||||||
|
|
||||||
|
data_[head_] = std::move(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_front() {
|
||||||
|
if (!empty())
|
||||||
|
pop_front_internal();
|
||||||
|
}
|
||||||
|
|
||||||
|
void push_back(T val) {
|
||||||
|
data_[end_] = std::move(val);
|
||||||
|
|
||||||
|
end_ = end_ < last_index ? end_ + 1 : 0;
|
||||||
|
if (head_ == end_)
|
||||||
|
pop_front_internal();
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_back() {
|
||||||
|
if (!empty())
|
||||||
|
pop_back_internal();
|
||||||
|
}
|
||||||
|
|
||||||
|
T& operator[](size_t ix) & {
|
||||||
|
ix += head_;
|
||||||
|
if (ix >= Capacity)
|
||||||
|
ix -= Capacity;
|
||||||
|
return data_[ix];
|
||||||
|
}
|
||||||
|
|
||||||
|
const T& operator[](size_t ix) const& {
|
||||||
|
return const_cast<CircularBuffer*>(this)->operator[](ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
const T& front() const& {
|
||||||
|
return data_[head_];
|
||||||
|
}
|
||||||
|
|
||||||
|
const T& back() const& {
|
||||||
|
auto end = end_ > 0 ? end_ - 1 : last_index;
|
||||||
|
return data_[end];
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const& {
|
||||||
|
auto end = end_;
|
||||||
|
if (end < head_)
|
||||||
|
end += Capacity;
|
||||||
|
return end - head_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool empty() const {
|
||||||
|
return head_ == end_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
head_ = 0;
|
||||||
|
end_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void pop_front_internal() {
|
||||||
|
head_ = head_ < last_index ? head_ + 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_back_internal() {
|
||||||
|
end_ = end_ > 0 ? end_ - 1 : last_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr size_t last_index = Capacity - 1;
|
||||||
|
size_t head_{0};
|
||||||
|
size_t end_{0};
|
||||||
|
|
||||||
|
T data_[Capacity]{};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /*__CIRCULAR_BUFFER_H__*/
|
@ -28,14 +28,27 @@ template <typename T>
|
|||||||
class Optional {
|
class Optional {
|
||||||
public:
|
public:
|
||||||
constexpr Optional()
|
constexpr Optional()
|
||||||
: value_{}, valid_{false} {};
|
: value_{}, valid_{false} {}
|
||||||
constexpr Optional(const T& value)
|
constexpr Optional(const T& value)
|
||||||
: value_{value}, valid_{true} {};
|
: value_{value}, valid_{true} {}
|
||||||
constexpr Optional(T&& value)
|
constexpr Optional(T&& value)
|
||||||
: value_{std::move(value)}, valid_{true} {};
|
: value_{std::move(value)}, valid_{true} {}
|
||||||
|
|
||||||
bool is_valid() const { return valid_; };
|
bool is_valid() const { return valid_; }
|
||||||
const T& value() const { return value_; };
|
|
||||||
|
// TODO: Throw if not valid?
|
||||||
|
T& value() & { return value_; }
|
||||||
|
T& operator*() & { return value_; }
|
||||||
|
const T& value() const& { return value_; }
|
||||||
|
const T& operator*() const& { return value_; }
|
||||||
|
|
||||||
|
T&& value() && { return value_; }
|
||||||
|
T&& operator*() && { return value_; }
|
||||||
|
const T&& value() const&& { return value_; }
|
||||||
|
const T&& operator*() const&& { return value_; }
|
||||||
|
|
||||||
|
T* operator->() { return &value_; }
|
||||||
|
const T* operator->() const { return &value_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
T value_;
|
T value_;
|
||||||
|
@ -35,6 +35,7 @@ set(CMAKE_CXX_COMPILER g++)
|
|||||||
add_executable(application_test EXCLUDE_FROM_ALL
|
add_executable(application_test EXCLUDE_FROM_ALL
|
||||||
${PROJECT_SOURCE_DIR}/main.cpp
|
${PROJECT_SOURCE_DIR}/main.cpp
|
||||||
${PROJECT_SOURCE_DIR}/test_basics.cpp
|
${PROJECT_SOURCE_DIR}/test_basics.cpp
|
||||||
|
${PROJECT_SOURCE_DIR}/test_circular_buffer.cpp
|
||||||
${PROJECT_SOURCE_DIR}/test_optional.cpp
|
${PROJECT_SOURCE_DIR}/test_optional.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
252
firmware/test/application/test_circular_buffer.cpp
Normal file
252
firmware/test/application/test_circular_buffer.cpp
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "doctest.h"
|
||||||
|
#include "circular_buffer.hpp"
|
||||||
|
|
||||||
|
TEST_SUITE_BEGIN("circular buffer");
|
||||||
|
|
||||||
|
SCENARIO("Items can be pushed and popped from front and back.") {
|
||||||
|
GIVEN("an empty buffer") {
|
||||||
|
CircularBuffer<int, 5> cb;
|
||||||
|
|
||||||
|
REQUIRE(cb.empty());
|
||||||
|
REQUIRE(cb.size() == 0);
|
||||||
|
|
||||||
|
WHEN("push_back()") {
|
||||||
|
cb.push_back(1);
|
||||||
|
|
||||||
|
THEN("size should increase") {
|
||||||
|
CHECK(cb.empty() == false);
|
||||||
|
CHECK(cb.size() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cb.push_back(2);
|
||||||
|
|
||||||
|
THEN("size should increase") {
|
||||||
|
CHECK(cb.empty() == false);
|
||||||
|
CHECK(cb.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("back() should be the last item pushed") {
|
||||||
|
CHECK(cb.back() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("front() should be the first item pushed") {
|
||||||
|
CHECK(cb.front() == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WHEN("push_front()") {
|
||||||
|
cb.push_front(3);
|
||||||
|
|
||||||
|
THEN("size should increase") {
|
||||||
|
CHECK(cb.empty() == false);
|
||||||
|
CHECK(cb.size() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cb.push_front(4);
|
||||||
|
|
||||||
|
THEN("size should increase") {
|
||||||
|
CHECK(cb.empty() == false);
|
||||||
|
CHECK(cb.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("back() should be first item pushed") {
|
||||||
|
CHECK(cb.back() == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("front() should be the last item pushed") {
|
||||||
|
CHECK(cb.front() == 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WHEN("pop_back()") {
|
||||||
|
cb.pop_back();
|
||||||
|
|
||||||
|
THEN("size should not change") {
|
||||||
|
REQUIRE(cb.empty());
|
||||||
|
REQUIRE(cb.size() == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WHEN("pop_front()") {
|
||||||
|
cb.pop_back();
|
||||||
|
|
||||||
|
THEN("size should not change") {
|
||||||
|
REQUIRE(cb.empty());
|
||||||
|
REQUIRE(cb.size() == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GIVEN("a buffer with items") {
|
||||||
|
CircularBuffer<int, 5> cb;
|
||||||
|
cb.push_back(1);
|
||||||
|
cb.push_back(2);
|
||||||
|
cb.push_back(3);
|
||||||
|
|
||||||
|
REQUIRE(!cb.empty());
|
||||||
|
REQUIRE(cb.size() == 3);
|
||||||
|
REQUIRE(cb.front() == 1);
|
||||||
|
REQUIRE(cb.back() == 3);
|
||||||
|
|
||||||
|
WHEN("pop_back()") {
|
||||||
|
cb.pop_back();
|
||||||
|
|
||||||
|
THEN("size should decrease") {
|
||||||
|
CHECK(cb.empty() == false);
|
||||||
|
CHECK(cb.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("back item should be removed") {
|
||||||
|
CHECK(cb.back() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("front item should be unchanged") {
|
||||||
|
CHECK(cb.front() == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WHEN("pop_front()") {
|
||||||
|
cb.pop_front();
|
||||||
|
|
||||||
|
THEN("size should decrease") {
|
||||||
|
CHECK(cb.empty() == false);
|
||||||
|
CHECK(cb.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("front item should be removed") {
|
||||||
|
CHECK(cb.front() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("back item should be unchanged") {
|
||||||
|
CHECK(cb.back() == 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WHEN("clear()") {
|
||||||
|
cb.clear();
|
||||||
|
|
||||||
|
THEN("size should be empty") {
|
||||||
|
CHECK(cb.empty());
|
||||||
|
CHECK(cb.size() == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GIVEN("a full buffer") {
|
||||||
|
CircularBuffer<int, 4> cb;
|
||||||
|
cb.push_back(1);
|
||||||
|
cb.push_back(2);
|
||||||
|
cb.push_back(3);
|
||||||
|
|
||||||
|
REQUIRE(!cb.empty());
|
||||||
|
REQUIRE(cb.size() == 3);
|
||||||
|
REQUIRE(cb.front() == 1);
|
||||||
|
REQUIRE(cb.back() == 3);
|
||||||
|
|
||||||
|
WHEN("push_back()") {
|
||||||
|
cb.push_back(4);
|
||||||
|
|
||||||
|
THEN("size should not be changed") {
|
||||||
|
CHECK(!cb.empty());
|
||||||
|
CHECK(cb.size() == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("front should be popped") {
|
||||||
|
CHECK(cb.front() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("back should be new value") {
|
||||||
|
CHECK(cb.back() == 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WHEN("push_front()") {
|
||||||
|
cb.push_front(4);
|
||||||
|
|
||||||
|
THEN("size should not be changed") {
|
||||||
|
CHECK(!cb.empty());
|
||||||
|
CHECK(cb.size() == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("front should be new value") {
|
||||||
|
CHECK(cb.front() == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("back should be popped") {
|
||||||
|
CHECK(cb.back() == 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SCENARIO("Items can be accessed randomly") {
|
||||||
|
GIVEN("buffer with items") {
|
||||||
|
CircularBuffer<int, 4> cb;
|
||||||
|
cb.push_front(1);
|
||||||
|
cb.push_back(2);
|
||||||
|
cb.push_back(3);
|
||||||
|
|
||||||
|
WHEN("accessing items") {
|
||||||
|
THEN("front should be at 0") {
|
||||||
|
CHECK(cb.front() == cb[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("last should be at size() - 1") {
|
||||||
|
CHECK(cb.back() == cb[cb.size() - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("all should be accessible") {
|
||||||
|
// Assumes values are in order.
|
||||||
|
for (size_t i = 0; i < cb.size(); i++)
|
||||||
|
CHECK(cb[i] == i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WHEN("accessing items after push_front") {
|
||||||
|
cb.push_front(4);
|
||||||
|
|
||||||
|
THEN("front should be at 0") {
|
||||||
|
CHECK(cb.front() == cb[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("last should be at size() - 1") {
|
||||||
|
CHECK(cb.back() == cb[cb.size() - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WHEN("accessing items after push_back") {
|
||||||
|
cb.push_back(4);
|
||||||
|
|
||||||
|
THEN("front should be at 0") {
|
||||||
|
CHECK(cb.front() == cb[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
THEN("last should be at size() - 1") {
|
||||||
|
CHECK(cb.back() == cb[cb.size() - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_SUITE_END();
|
Loading…
x
Reference in New Issue
Block a user