mirror of
https://github.com/esphome/esphome.git
synced 2024-12-12 08:24:55 +01:00
Rework text splitting and layout algorithm to be easier to read and work on
Fixes an bug where text would sometimes overflow the x axis in some cases
This commit is contained in:
parent
c9ec0a11c7
commit
b49453ef7e
2 changed files with 138 additions and 161 deletions
|
@ -51,7 +51,7 @@ void TextRunPanel::render_internal(display::Display *display, display::Rect boun
|
||||||
calculated->run->background_color_);
|
calculated->run->background_color_);
|
||||||
}
|
}
|
||||||
display->print(calculated->bounds.x, calculated->bounds.y, calculated->run->font_,
|
display->print(calculated->bounds.x, calculated->bounds.y, calculated->run->font_,
|
||||||
calculated->run->foreground_color_, display::TextAlign::TOP_LEFT, calculated->text_.c_str());
|
calculated->run->foreground_color_, display::TextAlign::TOP_LEFT, calculated->text.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->debug_outline_runs_) {
|
if (this->debug_outline_runs_) {
|
||||||
|
@ -62,207 +62,155 @@ void TextRunPanel::render_internal(display::Display *display, display::Rect boun
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CalculatedLayout TextRunPanel::determine_layout(display::Display *display, display::Rect bounds, bool apply_alignment) {
|
std::vector<std::shared_ptr<CalculatedTextRun>> TextRunPanel::split_runs_into_words() {
|
||||||
ESP_LOGV(TAG, "Determining layout for (%i, %i)", bounds.w, bounds.h);
|
std::vector<std::shared_ptr<CalculatedTextRun>> runs;
|
||||||
|
|
||||||
CalculatedLayout calculated_layout;
|
|
||||||
int x_offset = 0;
|
|
||||||
int y_offset = 0;
|
|
||||||
int current_line_max_height = 0;
|
|
||||||
int widest_line = 0;
|
|
||||||
int line_number = 0;
|
|
||||||
|
|
||||||
for (TextRunBase *run : this->text_runs_) {
|
for (TextRunBase *run : this->text_runs_) {
|
||||||
int x1;
|
|
||||||
int width;
|
|
||||||
int height;
|
|
||||||
int baseline;
|
|
||||||
std::string text = run->get_text();
|
std::string text = run->get_text();
|
||||||
|
|
||||||
run->font_->measure(text.c_str(), &width, &x1, &baseline, &height);
|
|
||||||
|
|
||||||
if ((x_offset + width) <= bounds.w) {
|
|
||||||
// Item fits on the current line
|
|
||||||
auto calculated = std::make_shared<CalculatedTextRun>(run, text, display::Rect(x_offset, y_offset, width, height),
|
|
||||||
baseline, line_number);
|
|
||||||
calculated_layout.runs.push_back(calculated);
|
|
||||||
|
|
||||||
x_offset += width;
|
|
||||||
widest_line = std::max(widest_line, x_offset);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
current_line_max_height = std::max(current_line_max_height, height);
|
|
||||||
|
|
||||||
ESP_LOGVV(TAG, "'%s' will not fit on the line. Finding break characters", text.c_str());
|
|
||||||
|
|
||||||
// Item extends beyond our desired bounds - need to add word by word
|
|
||||||
CanWrapAtCharacterArguments can_wrap_at_args(this, 0, text, ' ');
|
CanWrapAtCharacterArguments can_wrap_at_args(this, 0, text, ' ');
|
||||||
std::string partial_run;
|
|
||||||
|
int last_break = 0;
|
||||||
for (int i = 0; i < text.size(); i++) {
|
for (int i = 0; i < text.size(); i++) {
|
||||||
can_wrap_at_args.offset = i;
|
|
||||||
can_wrap_at_args.character = text.at(i);
|
can_wrap_at_args.character = text.at(i);
|
||||||
|
can_wrap_at_args.offset = i;
|
||||||
bool can_wrap = this->can_wrap_at_character_.value(can_wrap_at_args);
|
bool can_wrap = this->can_wrap_at_character_.value(can_wrap_at_args);
|
||||||
if (can_wrap) {
|
if (!can_wrap) {
|
||||||
ESP_LOGVV(TAG, "Can break at '%c'. String is '%s'", can_wrap_at_args.character, partial_run.c_str());
|
|
||||||
|
|
||||||
run->font_->measure(partial_run.c_str(), &width, &x1, &baseline, &height);
|
|
||||||
if ((x_offset + width) <= bounds.w) {
|
|
||||||
ESP_LOGVV(TAG, "... Fits! (%i, %i)", x_offset, y_offset);
|
|
||||||
|
|
||||||
// Item fits on the current line
|
|
||||||
current_line_max_height = std::max(current_line_max_height, height);
|
|
||||||
|
|
||||||
auto calculated = std::make_shared<CalculatedTextRun>(
|
|
||||||
run, partial_run, display::Rect(x_offset, y_offset, width, height), baseline, line_number);
|
|
||||||
calculated_layout.runs.push_back(calculated);
|
|
||||||
|
|
||||||
x_offset += width;
|
|
||||||
widest_line = std::max(widest_line, x_offset);
|
|
||||||
|
|
||||||
partial_run = can_wrap_at_args.character;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGVV(TAG, "... Doesn't fit - will overflow to next line");
|
auto calculated = std::make_shared<CalculatedTextRun>(run, text.substr(last_break, i - last_break));
|
||||||
|
calculated->calculate_bounds();
|
||||||
|
runs.push_back(calculated);
|
||||||
|
last_break = i;
|
||||||
|
}
|
||||||
|
|
||||||
// Overflows the current line
|
if (last_break < text.size()) {
|
||||||
|
auto calculated = std::make_shared<CalculatedTextRun>(run, text.substr(last_break));
|
||||||
|
calculated->calculate_bounds();
|
||||||
|
runs.push_back(calculated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return runs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<LineInfo>> TextRunPanel::fit_words_to_bounds(const std::vector<std::shared_ptr<CalculatedTextRun>> &runs, display::Rect bounds) {
|
||||||
|
int x_offset = 0;
|
||||||
|
int y_offset = 0;
|
||||||
|
int current_line_number = 0;
|
||||||
|
std::vector<std::shared_ptr<LineInfo>> lines;
|
||||||
|
|
||||||
|
auto current_line = std::make_shared<LineInfo>(current_line_number);
|
||||||
|
lines.push_back(current_line);
|
||||||
|
|
||||||
|
for (int i = 0; i < runs.size(); i++) {
|
||||||
|
const auto &run = runs.at(i);
|
||||||
|
if (run->bounds.w + x_offset > bounds.w) {
|
||||||
|
// 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;
|
||||||
line_number++;
|
|
||||||
current_line_max_height = height;
|
current_line_number++;
|
||||||
partial_run += can_wrap_at_args.character;
|
current_line = std::make_shared<LineInfo>(current_line_number);
|
||||||
continue;
|
|
||||||
|
lines.push_back(current_line);
|
||||||
}
|
}
|
||||||
|
|
||||||
partial_run += can_wrap_at_args.character;
|
// Fits on the line
|
||||||
|
run->bounds.x = x_offset;
|
||||||
|
run->bounds.y = y_offset;
|
||||||
|
|
||||||
|
current_line->add_run(run);
|
||||||
|
|
||||||
|
x_offset += run->bounds.w;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partial_run.length() > 0) {
|
return lines;
|
||||||
// Remaining text
|
|
||||||
run->font_->measure(partial_run.c_str(), &width, &x1, &baseline, &height);
|
|
||||||
|
|
||||||
current_line_max_height = std::max(height, current_line_max_height);
|
|
||||||
ESP_LOGVV(TAG, "'%s' is remaining after character break checks. Rendering to (%i, %i)", partial_run.c_str(),
|
|
||||||
x_offset, y_offset);
|
|
||||||
|
|
||||||
auto calculated = std::make_shared<CalculatedTextRun>(
|
|
||||||
run, partial_run, display::Rect(x_offset, y_offset, width, height), baseline, line_number);
|
|
||||||
calculated_layout.runs.push_back(calculated);
|
|
||||||
|
|
||||||
x_offset += width;
|
|
||||||
widest_line = std::max(widest_line, x_offset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
y_offset += current_line_max_height;
|
void TextRunPanel::apply_alignment_to_lines(std::vector<std::shared_ptr<LineInfo>> &lines, display::TextAlign alignment) {
|
||||||
|
|
||||||
calculated_layout.bounds = display::Rect(0, 0, widest_line, y_offset);
|
|
||||||
calculated_layout.line_count = line_number + 1;
|
|
||||||
if (calculated_layout.bounds.w < this->min_width_) {
|
|
||||||
calculated_layout.bounds.w = this->min_width_;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apply_alignment) {
|
|
||||||
this->apply_alignment_to_layout(&calculated_layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Measured layout is (%i, %i) (%i lines)", calculated_layout.bounds.w, calculated_layout.bounds.h,
|
|
||||||
calculated_layout.line_count);
|
|
||||||
|
|
||||||
return calculated_layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TextRunPanel::apply_alignment_to_layout(CalculatedLayout *calculated_layout) {
|
|
||||||
const auto x_align = display::TextAlign(int(this->text_align_) & TEXT_ALIGN_X_MASK);
|
const auto x_align = display::TextAlign(int(this->text_align_) & TEXT_ALIGN_X_MASK);
|
||||||
const auto y_align = display::TextAlign(int(this->text_align_) & TEXT_ALIGN_Y_MASK);
|
const auto y_align = display::TextAlign(int(this->text_align_) & TEXT_ALIGN_Y_MASK);
|
||||||
|
|
||||||
ESP_LOGVV(TAG, "We have %i lines to apply alignment to!", calculated_layout->line_count);
|
int16_t max_line_width = 0;
|
||||||
|
int16_t total_height = 0;
|
||||||
int total_y_offset = 0;
|
for (const auto &line : lines) {
|
||||||
|
max_line_width = std::max(line->total_width, max_line_width);
|
||||||
for (int i = 0; i < calculated_layout->line_count; i++) {
|
total_height += line->max_height;
|
||||||
std::vector<std::shared_ptr<CalculatedTextRun>> line_runs;
|
|
||||||
|
|
||||||
// Get all the runs for the current line
|
|
||||||
for (const auto &run : calculated_layout->runs) {
|
|
||||||
if (run->line_number_ == i) {
|
|
||||||
line_runs.push_back(run);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGVV(TAG, "Found %i runs on line %i", line_runs.size(), i);
|
int total_y_adjustment = 0;
|
||||||
|
for (const auto &line : lines) {
|
||||||
int16_t total_line_width = 0;
|
|
||||||
int16_t max_line_height = 0;
|
|
||||||
int16_t max_baseline = 0;
|
|
||||||
for (const auto &run : line_runs) {
|
|
||||||
total_line_width += run->bounds.w;
|
|
||||||
max_line_height = std::max(run->bounds.h, max_line_height);
|
|
||||||
max_baseline = std::max(run->baseline, max_baseline);
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGVV(TAG, "Line %i totals (%i, %i) pixels of (%i, %i)", i, total_line_width, max_line_height,
|
|
||||||
calculated_layout->bounds.w, calculated_layout->bounds.h);
|
|
||||||
|
|
||||||
int x_adjustment = 0;
|
int x_adjustment = 0;
|
||||||
int y_adjustment = 0;
|
int y_adjustment = 0;
|
||||||
|
int max_line_y_adjustment = 0;
|
||||||
|
|
||||||
|
// Horizontal alignment
|
||||||
switch (x_align) {
|
switch (x_align) {
|
||||||
case display::TextAlign::RIGHT: {
|
case display::TextAlign::RIGHT: {
|
||||||
x_adjustment = calculated_layout->bounds.w - total_line_width;
|
x_adjustment = max_line_width - line->total_width;
|
||||||
ESP_LOGVV(TAG, "Will adjust line %i by %i x-pixels", i, x_adjustment);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case display::TextAlign::CENTER_HORIZONTAL: {
|
case display::TextAlign::CENTER_HORIZONTAL: {
|
||||||
x_adjustment = (calculated_layout->bounds.w - total_line_width) / 2;
|
x_adjustment = (max_line_width - line->total_width) / 2;
|
||||||
ESP_LOGVV(TAG, "Will adjust line %i by %i x-pixels", i, x_adjustment);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case display::TextAlign::LEFT:
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int max_line_y_adjustment = 0;
|
// Perform adjustment
|
||||||
for (const auto &run : line_runs) {
|
for (const auto &run : line->runs) {
|
||||||
ESP_LOGVV(TAG, "Adjusting '%s' from (%i, %i) to (%i, %i)", run->text_.c_str(), run->bounds.x, run->bounds.y,
|
|
||||||
run->bounds.x + x_adjustment, run->bounds.y + y_adjustment);
|
|
||||||
run->bounds.x += x_adjustment;
|
|
||||||
|
|
||||||
switch (y_align) {
|
switch (y_align) {
|
||||||
case display::TextAlign::BOTTOM: {
|
case display::TextAlign::BOTTOM: {
|
||||||
y_adjustment = max_line_height - run->bounds.h;
|
y_adjustment = line->max_height - run->bounds.h;
|
||||||
ESP_LOGVV(TAG, "Will adjust line %i by %i y-pixels (%i vs %i)", i, y_adjustment, max_line_height,
|
|
||||||
run->bounds.h);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case display::TextAlign::CENTER_VERTICAL: {
|
case display::TextAlign::CENTER_VERTICAL: {
|
||||||
y_adjustment = (max_line_height - run->bounds.h) / 2;
|
y_adjustment = (line->max_height - run->bounds.h) / 2;
|
||||||
ESP_LOGVV(TAG, "Will adjust line %i by %i y-pixels", i, y_adjustment);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case display::TextAlign::BASELINE: {
|
case display::TextAlign::BASELINE: {
|
||||||
// Adjust this run based on its difference from the maximum baseline in the line
|
y_adjustment = line->max_baseline - run->baseline;
|
||||||
y_adjustment = max_baseline - run->baseline;
|
|
||||||
ESP_LOGVV(TAG, "Will adjust '%s' by %i y-pixels (ML: %i, H: %i, BL: %i)", run->text_.c_str(), y_adjustment,
|
|
||||||
max_line_height, run->bounds.h, run->baseline);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case display::TextAlign::TOP:
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run->bounds.y += y_adjustment + total_y_offset;
|
run->bounds.x += x_adjustment;
|
||||||
|
run->bounds.y += y_adjustment + total_y_adjustment;
|
||||||
max_line_y_adjustment = std::max(max_line_y_adjustment, y_adjustment);
|
max_line_y_adjustment = std::max(max_line_y_adjustment, y_adjustment);
|
||||||
}
|
}
|
||||||
|
|
||||||
total_y_offset += max_line_y_adjustment;
|
total_y_adjustment += max_line_y_adjustment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
calculated_layout->bounds.h += total_y_offset;
|
CalculatedLayout TextRunPanel::determine_layout(display::Display *display, display::Rect bounds, bool apply_alignment) {
|
||||||
|
std::vector<std::shared_ptr<CalculatedTextRun>> runs = this->split_runs_into_words();
|
||||||
|
std::vector<std::shared_ptr<LineInfo>> lines = this->fit_words_to_bounds(runs, bounds);
|
||||||
|
this->apply_alignment_to_lines(lines, this->text_align_);
|
||||||
|
|
||||||
|
CalculatedLayout layout;
|
||||||
|
layout.runs = runs;
|
||||||
|
layout.line_count = lines.size();
|
||||||
|
|
||||||
|
int y_offset = 0;
|
||||||
|
layout.bounds = display::Rect(0, 0, 0, 0);
|
||||||
|
for (const auto &line : lines) {
|
||||||
|
y_offset += line->max_height;
|
||||||
|
layout.bounds.w = std::max(layout.bounds.w, line->total_width);
|
||||||
|
}
|
||||||
|
layout.bounds.h = y_offset;
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Text fits on %i lines and its bounds are (%i, %i)", layout.line_count, layout.bounds.w, layout.bounds.h);
|
||||||
|
|
||||||
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TextRunPanel::default_can_wrap_at_character(const CanWrapAtCharacterArguments &args) {
|
bool TextRunPanel::default_can_wrap_at_character(const CanWrapAtCharacterArguments &args) {
|
||||||
|
|
|
@ -98,19 +98,27 @@ class TextSensorTextRun : public TextRunBase, public FormattableTextRun {
|
||||||
|
|
||||||
class CalculatedTextRun {
|
class CalculatedTextRun {
|
||||||
public:
|
public:
|
||||||
CalculatedTextRun(TextRunBase *run, std::string text, display::Rect bounds, int16_t baseline, int16_t line_number) {
|
CalculatedTextRun(TextRunBase *run, std::string text) {
|
||||||
this->run = run;
|
this->run = run;
|
||||||
this->text_ = std::move(text);
|
this->text = std::move(text);
|
||||||
this->bounds = bounds;
|
|
||||||
this->baseline = baseline;
|
|
||||||
this->line_number_ = line_number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string text_;
|
void calculate_bounds() {
|
||||||
display::Rect bounds;
|
int x1;
|
||||||
TextRunBase *run;
|
int width;
|
||||||
int16_t line_number_;
|
int height;
|
||||||
int16_t baseline;
|
int baseline;
|
||||||
|
|
||||||
|
this->run->font_->measure(this->text.c_str(), &width, &x1, &baseline, &height);
|
||||||
|
|
||||||
|
this->baseline = baseline;
|
||||||
|
this->bounds = display::Rect(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string text{};
|
||||||
|
display::Rect bounds{};
|
||||||
|
TextRunBase *run{nullptr};
|
||||||
|
int16_t baseline{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CalculatedLayout {
|
struct CalculatedLayout {
|
||||||
|
@ -119,6 +127,24 @@ struct CalculatedLayout {
|
||||||
int line_count;
|
int line_count;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class LineInfo {
|
||||||
|
public:
|
||||||
|
LineInfo(int line_number) { this->line_number = line_number; }
|
||||||
|
|
||||||
|
void add_run(std::shared_ptr<CalculatedTextRun> run) {
|
||||||
|
this->total_width += run->bounds.w;
|
||||||
|
this->max_height = std::max(this->max_height, run->bounds.h);
|
||||||
|
this->max_baseline = std::max(this->max_baseline, run->baseline);
|
||||||
|
this->runs.push_back(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<CalculatedTextRun>> runs;
|
||||||
|
int16_t line_number{0};
|
||||||
|
int16_t max_height{0};
|
||||||
|
int16_t total_width{0};
|
||||||
|
int16_t max_baseline{0};
|
||||||
|
};
|
||||||
|
|
||||||
/** 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:
|
||||||
|
@ -129,9 +155,6 @@ class TextRunPanel : public LayoutItem {
|
||||||
|
|
||||||
bool default_can_wrap_at_character(const CanWrapAtCharacterArguments &args);
|
bool default_can_wrap_at_character(const CanWrapAtCharacterArguments &args);
|
||||||
|
|
||||||
CalculatedLayout determine_layout(display::Display *display, display::Rect bounds, bool apply_alignment);
|
|
||||||
void apply_alignment_to_layout(CalculatedLayout *layout);
|
|
||||||
|
|
||||||
template<typename V> void set_can_wrap_at(V can_wrap_at_character) {
|
template<typename V> void set_can_wrap_at(V can_wrap_at_character) {
|
||||||
this->can_wrap_at_character_ = can_wrap_at_character;
|
this->can_wrap_at_character_ = can_wrap_at_character;
|
||||||
};
|
};
|
||||||
|
@ -143,6 +166,12 @@ class TextRunPanel : public LayoutItem {
|
||||||
void set_debug_outline_runs(bool debug_outline_runs) { this->debug_outline_runs_ = debug_outline_runs; };
|
void set_debug_outline_runs(bool debug_outline_runs) { this->debug_outline_runs_ = debug_outline_runs; };
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
CalculatedLayout determine_layout(display::Display *display, display::Rect bounds, bool apply_alignment);
|
||||||
|
std::vector<std::shared_ptr<CalculatedTextRun>> split_runs_into_words();
|
||||||
|
std::vector<std::shared_ptr<LineInfo>> fit_words_to_bounds(
|
||||||
|
const std::vector<std::shared_ptr<CalculatedTextRun>> &runs, display::Rect bounds);
|
||||||
|
void apply_alignment_to_lines(std::vector<std::shared_ptr<LineInfo>> &lines, display::TextAlign alignment);
|
||||||
|
|
||||||
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};
|
||||||
int min_width_{0};
|
int min_width_{0};
|
||||||
|
|
Loading…
Reference in a new issue