From b49453ef7eec8f0b704944aa49cdf0f6d7000b91 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Sat, 6 Jan 2024 11:31:07 +1100 Subject: [PATCH] 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 --- .../graphical_layout/text_run_panel.cpp | 244 +++++++----------- .../graphical_layout/text_run_panel.h | 55 +++- 2 files changed, 138 insertions(+), 161 deletions(-) diff --git a/esphome/components/graphical_layout/text_run_panel.cpp b/esphome/components/graphical_layout/text_run_panel.cpp index 7fe61b74bf..b25e5b71a5 100644 --- a/esphome/components/graphical_layout/text_run_panel.cpp +++ b/esphome/components/graphical_layout/text_run_panel.cpp @@ -51,7 +51,7 @@ void TextRunPanel::render_internal(display::Display *display, display::Rect boun calculated->run->background_color_); } 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_) { @@ -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) { - ESP_LOGV(TAG, "Determining layout for (%i, %i)", bounds.w, bounds.h); - - 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; +std::vector> TextRunPanel::split_runs_into_words() { + std::vector> runs; for (TextRunBase *run : this->text_runs_) { - int x1; - int width; - int height; - int baseline; 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(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, ' '); - std::string partial_run; + + int last_break = 0; 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.offset = i; bool can_wrap = this->can_wrap_at_character_.value(can_wrap_at_args); - 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( - 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; - } - - ESP_LOGVV(TAG, "... Doesn't fit - will overflow to next line"); - - // Overflows the current line - x_offset = 0; - y_offset += current_line_max_height; - line_number++; - current_line_max_height = height; - partial_run += can_wrap_at_args.character; + if (!can_wrap) { continue; } - partial_run += can_wrap_at_args.character; + auto calculated = std::make_shared(run, text.substr(last_break, i - last_break)); + calculated->calculate_bounds(); + runs.push_back(calculated); + last_break = i; } - if (partial_run.length() > 0) { - // 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( - 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); + if (last_break < text.size()) { + auto calculated = std::make_shared(run, text.substr(last_break)); + calculated->calculate_bounds(); + runs.push_back(calculated); } } - y_offset += current_line_max_height; - - 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; + return runs; } -void TextRunPanel::apply_alignment_to_layout(CalculatedLayout *calculated_layout) { +std::vector> TextRunPanel::fit_words_to_bounds(const std::vector> &runs, display::Rect bounds) { + int x_offset = 0; + int y_offset = 0; + int current_line_number = 0; + std::vector> lines; + + auto current_line = std::make_shared(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; + y_offset += current_line->max_height; + + current_line_number++; + current_line = std::make_shared(current_line_number); + + lines.push_back(current_line); + } + + // Fits on the line + run->bounds.x = x_offset; + run->bounds.y = y_offset; + + current_line->add_run(run); + + x_offset += run->bounds.w; + } + + return lines; +} + +void TextRunPanel::apply_alignment_to_lines(std::vector> &lines, display::TextAlign alignment) { 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); - ESP_LOGVV(TAG, "We have %i lines to apply alignment to!", calculated_layout->line_count); - - int total_y_offset = 0; - - for (int i = 0; i < calculated_layout->line_count; i++) { - std::vector> 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); - - 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); + int16_t max_line_width = 0; + int16_t total_height = 0; + for (const auto &line : lines) { + max_line_width = std::max(line->total_width, max_line_width); + total_height += line->max_height; + } + int total_y_adjustment = 0; + for (const auto &line : lines) { int x_adjustment = 0; int y_adjustment = 0; + int max_line_y_adjustment = 0; + + // Horizontal alignment switch (x_align) { case display::TextAlign::RIGHT: { - x_adjustment = calculated_layout->bounds.w - total_line_width; - ESP_LOGVV(TAG, "Will adjust line %i by %i x-pixels", i, x_adjustment); - break; + x_adjustment = max_line_width - line->total_width; + break; } case display::TextAlign::CENTER_HORIZONTAL: { - x_adjustment = (calculated_layout->bounds.w - total_line_width) / 2; - ESP_LOGVV(TAG, "Will adjust line %i by %i x-pixels", i, x_adjustment); + x_adjustment = (max_line_width - line->total_width) / 2; break; } + case display::TextAlign::LEFT: default: { break; } } - int max_line_y_adjustment = 0; - 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; - + // Perform adjustment + for (const auto &run : line->runs) { switch (y_align) { case display::TextAlign::BOTTOM: { - y_adjustment = max_line_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); + y_adjustment = line->max_height - run->bounds.h; break; } case display::TextAlign::CENTER_VERTICAL: { - y_adjustment = (max_line_height - run->bounds.h) / 2; - ESP_LOGVV(TAG, "Will adjust line %i by %i y-pixels", i, y_adjustment); + y_adjustment = (line->max_height - run->bounds.h) / 2; break; } case display::TextAlign::BASELINE: { - // Adjust this run based on its difference from the maximum baseline in the line - 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); + y_adjustment = line->max_baseline - run->baseline; break; } + case display::TextAlign::TOP: default: { 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); } - 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> runs = this->split_runs_into_words(); + std::vector> 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) { diff --git a/esphome/components/graphical_layout/text_run_panel.h b/esphome/components/graphical_layout/text_run_panel.h index 639618aad1..525225b92a 100644 --- a/esphome/components/graphical_layout/text_run_panel.h +++ b/esphome/components/graphical_layout/text_run_panel.h @@ -98,19 +98,27 @@ class TextSensorTextRun : public TextRunBase, public FormattableTextRun { class CalculatedTextRun { 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->text_ = std::move(text); - this->bounds = bounds; - this->baseline = baseline; - this->line_number_ = line_number; + this->text = std::move(text); } - std::string text_; - display::Rect bounds; - TextRunBase *run; - int16_t line_number_; - int16_t baseline; + void calculate_bounds() { + int x1; + int width; + int height; + 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 { @@ -119,6 +127,24 @@ struct CalculatedLayout { int line_count; }; +class LineInfo { + public: + LineInfo(int line_number) { this->line_number = line_number; } + + void add_run(std::shared_ptr 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> 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 */ class TextRunPanel : public LayoutItem { public: @@ -129,9 +155,6 @@ class TextRunPanel : public LayoutItem { 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 void set_can_wrap_at(V 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; }; protected: + CalculatedLayout determine_layout(display::Display *display, display::Rect bounds, bool apply_alignment); + std::vector> split_runs_into_words(); + std::vector> fit_words_to_bounds( + const std::vector> &runs, display::Rect bounds); + void apply_alignment_to_lines(std::vector> &lines, display::TextAlign alignment); + std::vector text_runs_; display::TextAlign text_align_{display::TextAlign::TOP_LEFT}; int min_width_{0};