197 lines
5.7 KiB
C++
197 lines
5.7 KiB
C++
#include <algorithm>
|
|
#include <cctype>
|
|
#include <faset/ui/ui.hpp>
|
|
#include <stdexcept>
|
|
|
|
namespace faset::ui {
|
|
namespace {
|
|
bool continuation(unsigned char c) {
|
|
return (c & 0xc0) == 0x80;
|
|
}
|
|
std::size_t previous(std::string_view text, std::size_t at) {
|
|
if (!at)
|
|
return 0;
|
|
--at;
|
|
while (at && continuation(static_cast<unsigned char>(text[at])))
|
|
--at;
|
|
return at;
|
|
}
|
|
std::size_t next(std::string_view text, std::size_t at) {
|
|
if (at >= text.size())
|
|
return text.size();
|
|
++at;
|
|
while (at < text.size() && continuation(static_cast<unsigned char>(text[at])))
|
|
++at;
|
|
return at;
|
|
}
|
|
bool word(unsigned char c) {
|
|
return c >= 128 || std::isalnum(c) || c == '_';
|
|
}
|
|
} // namespace
|
|
bool TextBuffer::valid_utf8(std::string_view text) {
|
|
for (std::size_t i = 0; i < text.size();) {
|
|
const auto lead = static_cast<unsigned char>(text[i++]);
|
|
if (lead < 128)
|
|
continue;
|
|
unsigned cp = 0;
|
|
int n = 0;
|
|
unsigned minimum = 0;
|
|
if ((lead & 0xe0) == 0xc0) {
|
|
cp = lead & 31;
|
|
n = 1;
|
|
minimum = 128;
|
|
} else if ((lead & 0xf0) == 0xe0) {
|
|
cp = lead & 15;
|
|
n = 2;
|
|
minimum = 2048;
|
|
} else if ((lead & 0xf8) == 0xf0) {
|
|
cp = lead & 7;
|
|
n = 3;
|
|
minimum = 65536;
|
|
} else
|
|
return false;
|
|
if (i + static_cast<std::size_t>(n) > text.size())
|
|
return false;
|
|
while (n--) {
|
|
auto c = static_cast<unsigned char>(text[i++]);
|
|
if (!continuation(c))
|
|
return false;
|
|
cp = (cp << 6) | (c & 63);
|
|
}
|
|
if (cp < minimum || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
TextBuffer::TextBuffer(std::string text) {
|
|
reset(std::move(text));
|
|
}
|
|
void TextBuffer::reset(std::string text) {
|
|
if (!valid_utf8(text))
|
|
throw std::invalid_argument("Invalid UTF-8 text");
|
|
text_ = std::move(text);
|
|
cursor_ = anchor_ = text_.size();
|
|
undo_.clear();
|
|
redo_.clear();
|
|
}
|
|
void TextBuffer::set_cursor(std::size_t byte, bool select) {
|
|
cursor_ = std::min(byte, text_.size());
|
|
while (cursor_ && cursor_ < text_.size() &&
|
|
continuation(static_cast<unsigned char>(text_[cursor_])))
|
|
--cursor_;
|
|
if (!select)
|
|
anchor_ = cursor_;
|
|
}
|
|
void TextBuffer::select_all() {
|
|
anchor_ = 0;
|
|
cursor_ = text_.size();
|
|
}
|
|
std::string TextBuffer::selected_text() const {
|
|
return text_.substr(std::min(cursor_, anchor_),
|
|
std::max(cursor_, anchor_) - std::min(cursor_, anchor_));
|
|
}
|
|
void TextBuffer::left(bool select, bool by_word) {
|
|
if (!select && has_selection()) {
|
|
set_cursor(std::min(cursor_, anchor_));
|
|
return;
|
|
}
|
|
auto position = previous(text_, cursor_);
|
|
if (by_word) {
|
|
while (position && !word(static_cast<unsigned char>(text_[position])))
|
|
position = previous(text_, position);
|
|
while (position && word(static_cast<unsigned char>(text_[previous(text_, position)])))
|
|
position = previous(text_, position);
|
|
}
|
|
set_cursor(position, select);
|
|
}
|
|
void TextBuffer::right(bool select, bool by_word) {
|
|
if (!select && has_selection()) {
|
|
set_cursor(std::max(cursor_, anchor_));
|
|
return;
|
|
}
|
|
auto position = next(text_, cursor_);
|
|
if (by_word) {
|
|
while (position < text_.size() && word(static_cast<unsigned char>(text_[position])))
|
|
position = next(text_, position);
|
|
while (position < text_.size() && !word(static_cast<unsigned char>(text_[position])))
|
|
position = next(text_, position);
|
|
}
|
|
set_cursor(position, select);
|
|
}
|
|
void TextBuffer::home(bool select) {
|
|
set_cursor(0, select);
|
|
}
|
|
void TextBuffer::end(bool select) {
|
|
set_cursor(text_.size(), select);
|
|
}
|
|
void TextBuffer::remember() {
|
|
undo_.push_back({text_, cursor_, anchor_});
|
|
if (undo_.size() > 256)
|
|
undo_.erase(undo_.begin());
|
|
redo_.clear();
|
|
}
|
|
void TextBuffer::erase_selection() {
|
|
const auto begin = std::min(cursor_, anchor_), end = std::max(cursor_, anchor_);
|
|
text_.erase(begin, end - begin);
|
|
cursor_ = anchor_ = begin;
|
|
}
|
|
bool TextBuffer::insert(std::string_view utf8) {
|
|
if (!valid_utf8(utf8) || text_.size() + utf8.size() > 1024 * 1024)
|
|
return false;
|
|
if (utf8.empty() && !has_selection())
|
|
return false;
|
|
remember();
|
|
erase_selection();
|
|
text_.insert(cursor_, utf8);
|
|
cursor_ += utf8.size();
|
|
anchor_ = cursor_;
|
|
return true;
|
|
}
|
|
bool TextBuffer::backspace() {
|
|
if (!has_selection() && !cursor_)
|
|
return false;
|
|
remember();
|
|
if (has_selection())
|
|
erase_selection();
|
|
else {
|
|
auto begin = previous(text_, cursor_);
|
|
text_.erase(begin, cursor_ - begin);
|
|
cursor_ = anchor_ = begin;
|
|
}
|
|
return true;
|
|
}
|
|
bool TextBuffer::delete_forward() {
|
|
if (!has_selection() && cursor_ == text_.size())
|
|
return false;
|
|
remember();
|
|
if (has_selection())
|
|
erase_selection();
|
|
else
|
|
text_.erase(cursor_, next(text_, cursor_) - cursor_);
|
|
anchor_ = cursor_;
|
|
return true;
|
|
}
|
|
bool TextBuffer::undo() {
|
|
if (undo_.empty())
|
|
return false;
|
|
redo_.push_back({text_, cursor_, anchor_});
|
|
auto old = std::move(undo_.back());
|
|
undo_.pop_back();
|
|
text_ = std::move(old.text);
|
|
cursor_ = old.cursor;
|
|
anchor_ = old.anchor;
|
|
return true;
|
|
}
|
|
bool TextBuffer::redo() {
|
|
if (redo_.empty())
|
|
return false;
|
|
undo_.push_back({text_, cursor_, anchor_});
|
|
auto old = std::move(redo_.back());
|
|
redo_.pop_back();
|
|
text_ = std::move(old.text);
|
|
cursor_ = old.cursor;
|
|
anchor_ = old.anchor;
|
|
return true;
|
|
}
|
|
} // namespace faset::ui
|