Introduce calculated metadata about runs. Allows replacement (eg. \t to ' '), new line handling, and suppression of non-printable characters

This commit is contained in:
Michael Davidson 2024-01-23 22:07:48 +11:00
parent 29d4984b33
commit a1d613948a
No known key found for this signature in database
GPG key ID: B8D1A99712B8B0EB
2 changed files with 160 additions and 18 deletions

View file

@ -59,6 +59,9 @@ void TextRunPanel::render_internal(display::Display *display, display::Rect boun
}
for (const auto &calculated : line->runs) {
if (!calculated->run_properties.is_printable) {
continue;
}
if (calculated->run->background_color_ != display::COLOR_OFF) {
display->filled_rectangle(calculated->bounds.x, calculated->bounds.y, calculated->bounds.w,
calculated->bounds.h, calculated->run->background_color_);
@ -82,31 +85,37 @@ void TextRunPanel::render_internal(display::Display *display, display::Rect boun
std::vector<std::shared_ptr<CalculatedTextRun>> TextRunPanel::split_runs_into_words_() {
std::vector<std::shared_ptr<CalculatedTextRun>> runs;
for (TextRunBase *run : this->text_runs_) {
std::string text = run->get_text();
CanWrapAtCharacterArguments can_wrap_at_args(this, 0, text, ' ');
int last_break = 0;
std::shared_ptr<CalculatedTextRun> current_text_run = nullptr;
for (int i = 0; i < text.size(); i++) {
can_wrap_at_args.character = text.at(i);
char current_char = text.at(i);
CharacterProperties prop = this->get_character_properties_(current_char);
can_wrap_at_args.character = current_char;
can_wrap_at_args.offset = i;
bool can_wrap = this->can_wrap_at_character_.value(can_wrap_at_args);
if (!can_wrap) {
continue;
prop.can_wrap = this->can_wrap_at_character_.value(can_wrap_at_args);
if ((current_text_run == nullptr) || (current_text_run->run_properties.is_equivalent(prop) == false)) {
current_text_run = std::make_shared<CalculatedTextRun>(run, prop);
runs.push_back(current_text_run);
}
auto calculated = std::make_shared<CalculatedTextRun>(run, text.substr(last_break, i - last_break));
calculated->calculate_bounds();
runs.push_back(calculated);
last_break = i;
if (prop.is_printable) {
if (prop.replace_with.has_value()) {
ESP_LOGD(TAG, "Replacing '%c' (0x%x) with '%s'", current_char, current_char,
prop.replace_with.value().c_str());
current_text_run->text.append(prop.replace_with.value());
} else {
current_text_run->text.push_back(current_char);
}
}
}
}
if (last_break < text.size()) {
auto calculated = std::make_shared<CalculatedTextRun>(run, text.substr(last_break));
calculated->calculate_bounds();
runs.push_back(calculated);
}
for (const auto &run : runs) {
run->calculate_bounds();
}
return runs;
@ -122,17 +131,35 @@ std::vector<std::shared_ptr<LineInfo>> TextRunPanel::fit_words_to_bounds_(
auto current_line = std::make_shared<LineInfo>(current_line_number);
lines.push_back(current_line);
bool is_first_run_of_line = true;
for (const auto &run : runs) {
if (run->bounds.w + x_offset > bounds.w) {
if ((run->run_properties.causes_new_line) || (run->bounds.w + x_offset > bounds.w)) {
// Overflows the current line create a new line
x_offset = 0;
y_offset += current_line->max_height;
is_first_run_of_line = true;
// Handle runs at the end of the line that want to be suppressed
std::shared_ptr<CalculatedTextRun> last_run_of_line = current_line->runs.back();
bool run_requires_recalculation = false;
while (last_run_of_line->run_properties.suppress_at_end_of_line) {
ESP_LOGD(TAG, "Suppressing run for '%s' as it's the end of a line", last_run_of_line->text.c_str());
current_line->pop_last_run();
last_run_of_line = current_line->runs.back();
run_requires_recalculation = true;
}
if (run_requires_recalculation) {
current_line->recalculate_line_measurements();
}
current_line_number++;
current_line = std::make_shared<LineInfo>(current_line_number);
lines.push_back(current_line);
ESP_LOGD(TAG, "%i: Is New line: %s", current_line_number - 1, YESNO(run->run_properties.causes_new_line));
ESP_LOGD(TAG, "Line %i finishes at %i vs available of %i", current_line_number - 1, y_offset, bounds.h);
if (!grow_beyond_bounds_height && y_offset >= bounds.h) {
ESP_LOGD(TAG, "No more text can fit into the available height. Aborting");
@ -144,9 +171,15 @@ std::vector<std::shared_ptr<LineInfo>> TextRunPanel::fit_words_to_bounds_(
run->bounds.x = x_offset;
run->bounds.y = y_offset;
if (is_first_run_of_line && run->run_properties.suppress_at_start_of_line) {
ESP_LOGD(TAG, "Suppressing run for '%s' as it's the start of a line", run->text.c_str());
continue;
}
current_line->add_run(run);
x_offset += run->bounds.w;
is_first_run_of_line = false;
}
return lines;
@ -237,6 +270,54 @@ CalculatedLayout TextRunPanel::determine_layout_(display::Display *display, disp
return layout;
}
inline CharacterProperties TextRunPanel::get_character_properties_(char character) {
CharacterProperties props;
props.character = character;
if (character == '\t') {
// Replace tabs with 4 spaces
props.replace_with = std::string(" ");
props.is_white_space = true;
props.is_printable = true;
props.suppress_at_end_of_line = false;
props.suppress_at_start_of_line = false;
return props;
}
// New line/Carriage Return are treated identically
if ((character == '\n') || (character == '\r')) {
props.is_white_space = true;
// Don't display anything instead control new line with the causes_new_line
props.replace_with = std::string("");
props.causes_new_line = true;
props.is_printable = false;
return props;
}
// ASCII table is non-printable below space
// 0x7f is the DEL character and the end of the normal ASCII set
if ((character < ' ') || (character >= 0x7f)) {
props.replace_with = std::string("");
props.is_printable = false;
return props;
}
if (character == ' ') {
// Ensure we don't print at the start/end of the line. Wrapping to the next line is space enough
props.suppress_at_end_of_line = true;
props.suppress_at_start_of_line = true;
props.is_printable = true;
props.is_white_space = true;
return props;
}
// Everything else should be printable
props.is_printable = true;
return props;
}
bool TextRunPanel::default_can_wrap_at_character(const CanWrapAtCharacterArguments &args) {
switch (args.character) {
case ' ':

View file

@ -124,11 +124,47 @@ class TimeTextRun : public TextRunBase, public FormattableTextRun {
bool use_utc_time_{false};
};
struct RunProperties {
bool is_white_space{false};
/**
* If false the run will be skipped
*/
bool is_printable{false};
/*
* A run with this marked as true will not be printed when it appears at the start of a line. Useful for whitespace
*/
bool suppress_at_start_of_line{false};
/*
* A run with this marked as true will not be printed when it appears at the end of a line. Useful for white space
*/
bool suppress_at_end_of_line{false};
/**
* If true can allow the the line to end
*/
bool can_wrap{false};
/**
* If true will force a new line
*/
bool causes_new_line{false};
bool is_equivalent(const RunProperties &compare) {
return this->is_white_space == compare.is_white_space && this->is_printable == compare.is_printable &&
this->suppress_at_start_of_line == compare.suppress_at_start_of_line &&
this->suppress_at_end_of_line == compare.suppress_at_end_of_line && this->can_wrap == compare.can_wrap &&
this->causes_new_line == compare.causes_new_line;
}
};
class CalculatedTextRun {
public:
CalculatedTextRun(TextRunBase *run, std::string text) {
CalculatedTextRun(TextRunBase *run, RunProperties run_properties) {
this->run = run;
this->text = std::move(text);
this->run_properties = run_properties;
}
void calculate_bounds() {
@ -147,6 +183,7 @@ class CalculatedTextRun {
display::Rect bounds{};
TextRunBase *run{nullptr};
int16_t baseline{0};
RunProperties run_properties{};
};
class LineInfo {
@ -160,6 +197,24 @@ class LineInfo {
this->runs.push_back(run);
}
void pop_last_run() { this->runs.pop_back(); }
void recalculate_line_measurements() {
int16_t total_width = 0;
int16_t max_height = 0;
int16_t max_baseline = 0;
for (const auto &run : this->runs) {
total_width += run->bounds.w;
max_height = std::max(max_height, run->bounds.h);
max_baseline = std::max(max_baseline, run->baseline);
}
this->total_width = total_width;
this->max_height = max_height;
this->max_baseline = max_baseline;
}
std::vector<std::shared_ptr<CalculatedTextRun>> runs;
int16_t line_number{0};
int16_t max_height{0};
@ -172,6 +227,11 @@ struct CalculatedLayout {
display::Rect bounds;
};
struct CharacterProperties : RunProperties {
char character{'\0'};
optional<std::string> replace_with{};
};
/** The TextRunPanel is a UI item that renders a multiple "runs" of text of independent styling to a display */
class TextRunPanel : public LayoutItem {
public:
@ -200,6 +260,7 @@ class TextRunPanel : public LayoutItem {
const std::vector<std::shared_ptr<CalculatedTextRun>> &runs, display::Rect bounds,
bool grow_beyond_bounds_height);
void apply_alignment_to_lines_(std::vector<std::shared_ptr<LineInfo>> &lines, display::TextAlign alignment);
CharacterProperties get_character_properties_(char character);
std::vector<TextRunBase *> text_runs_;
display::TextAlign text_align_{display::TextAlign::TOP_LEFT};