mirror of
https://github.com/esphome/esphome.git
synced 2024-12-12 08:24:55 +01:00
Introduce calculated metadata about runs. Allows replacement (eg. \t to ' '), new line handling, and suppression of non-printable characters
This commit is contained in:
parent
29d4984b33
commit
a1d613948a
2 changed files with 160 additions and 18 deletions
|
@ -59,6 +59,9 @@ void TextRunPanel::render_internal(display::Display *display, display::Rect boun
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &calculated : line->runs) {
|
for (const auto &calculated : line->runs) {
|
||||||
|
if (!calculated->run_properties.is_printable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (calculated->run->background_color_ != display::COLOR_OFF) {
|
if (calculated->run->background_color_ != display::COLOR_OFF) {
|
||||||
display->filled_rectangle(calculated->bounds.x, calculated->bounds.y, calculated->bounds.w,
|
display->filled_rectangle(calculated->bounds.x, calculated->bounds.y, calculated->bounds.w,
|
||||||
calculated->bounds.h, calculated->run->background_color_);
|
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>> TextRunPanel::split_runs_into_words_() {
|
||||||
std::vector<std::shared_ptr<CalculatedTextRun>> runs;
|
std::vector<std::shared_ptr<CalculatedTextRun>> runs;
|
||||||
|
|
||||||
for (TextRunBase *run : this->text_runs_) {
|
for (TextRunBase *run : this->text_runs_) {
|
||||||
std::string text = run->get_text();
|
std::string text = run->get_text();
|
||||||
CanWrapAtCharacterArguments can_wrap_at_args(this, 0, 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++) {
|
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;
|
can_wrap_at_args.offset = i;
|
||||||
bool can_wrap = this->can_wrap_at_character_.value(can_wrap_at_args);
|
prop.can_wrap = this->can_wrap_at_character_.value(can_wrap_at_args);
|
||||||
if (!can_wrap) {
|
|
||||||
continue;
|
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));
|
if (prop.is_printable) {
|
||||||
calculated->calculate_bounds();
|
if (prop.replace_with.has_value()) {
|
||||||
runs.push_back(calculated);
|
ESP_LOGD(TAG, "Replacing '%c' (0x%x) with '%s'", current_char, current_char,
|
||||||
last_break = i;
|
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()) {
|
for (const auto &run : runs) {
|
||||||
auto calculated = std::make_shared<CalculatedTextRun>(run, text.substr(last_break));
|
run->calculate_bounds();
|
||||||
calculated->calculate_bounds();
|
|
||||||
runs.push_back(calculated);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return runs;
|
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);
|
auto current_line = std::make_shared<LineInfo>(current_line_number);
|
||||||
lines.push_back(current_line);
|
lines.push_back(current_line);
|
||||||
|
|
||||||
|
bool is_first_run_of_line = true;
|
||||||
|
|
||||||
for (const auto &run : runs) {
|
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
|
// Overflows the current line create a new line
|
||||||
x_offset = 0;
|
x_offset = 0;
|
||||||
y_offset += current_line->max_height;
|
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_number++;
|
||||||
current_line = std::make_shared<LineInfo>(current_line_number);
|
current_line = std::make_shared<LineInfo>(current_line_number);
|
||||||
|
|
||||||
lines.push_back(current_line);
|
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);
|
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) {
|
if (!grow_beyond_bounds_height && y_offset >= bounds.h) {
|
||||||
ESP_LOGD(TAG, "No more text can fit into the available height. Aborting");
|
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.x = x_offset;
|
||||||
run->bounds.y = y_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);
|
current_line->add_run(run);
|
||||||
|
|
||||||
x_offset += run->bounds.w;
|
x_offset += run->bounds.w;
|
||||||
|
is_first_run_of_line = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
|
@ -237,6 +270,54 @@ CalculatedLayout TextRunPanel::determine_layout_(display::Display *display, disp
|
||||||
return layout;
|
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) {
|
bool TextRunPanel::default_can_wrap_at_character(const CanWrapAtCharacterArguments &args) {
|
||||||
switch (args.character) {
|
switch (args.character) {
|
||||||
case ' ':
|
case ' ':
|
||||||
|
|
|
@ -124,11 +124,47 @@ class TimeTextRun : public TextRunBase, public FormattableTextRun {
|
||||||
bool use_utc_time_{false};
|
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 {
|
class CalculatedTextRun {
|
||||||
public:
|
public:
|
||||||
CalculatedTextRun(TextRunBase *run, std::string text) {
|
CalculatedTextRun(TextRunBase *run, RunProperties run_properties) {
|
||||||
this->run = run;
|
this->run = run;
|
||||||
this->text = std::move(text);
|
this->run_properties = run_properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
void calculate_bounds() {
|
void calculate_bounds() {
|
||||||
|
@ -147,6 +183,7 @@ class CalculatedTextRun {
|
||||||
display::Rect bounds{};
|
display::Rect bounds{};
|
||||||
TextRunBase *run{nullptr};
|
TextRunBase *run{nullptr};
|
||||||
int16_t baseline{0};
|
int16_t baseline{0};
|
||||||
|
RunProperties run_properties{};
|
||||||
};
|
};
|
||||||
|
|
||||||
class LineInfo {
|
class LineInfo {
|
||||||
|
@ -160,6 +197,24 @@ class LineInfo {
|
||||||
this->runs.push_back(run);
|
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;
|
std::vector<std::shared_ptr<CalculatedTextRun>> runs;
|
||||||
int16_t line_number{0};
|
int16_t line_number{0};
|
||||||
int16_t max_height{0};
|
int16_t max_height{0};
|
||||||
|
@ -172,6 +227,11 @@ struct CalculatedLayout {
|
||||||
display::Rect bounds;
|
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 */
|
/** The TextRunPanel is a UI item that renders a multiple "runs" of text of independent styling to a display */
|
||||||
class TextRunPanel : public LayoutItem {
|
class TextRunPanel : public LayoutItem {
|
||||||
public:
|
public:
|
||||||
|
@ -200,6 +260,7 @@ class TextRunPanel : public LayoutItem {
|
||||||
const std::vector<std::shared_ptr<CalculatedTextRun>> &runs, display::Rect bounds,
|
const std::vector<std::shared_ptr<CalculatedTextRun>> &runs, display::Rect bounds,
|
||||||
bool grow_beyond_bounds_height);
|
bool grow_beyond_bounds_height);
|
||||||
void apply_alignment_to_lines_(std::vector<std::shared_ptr<LineInfo>> &lines, display::TextAlign alignment);
|
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_;
|
std::vector<TextRunBase *> text_runs_;
|
||||||
display::TextAlign text_align_{display::TextAlign::TOP_LEFT};
|
display::TextAlign text_align_{display::TextAlign::TOP_LEFT};
|
||||||
|
|
Loading…
Reference in a new issue