Ensure text does not render beyond the available bounds given to the TextRunPanel

Introduce draw_partial_lines option. If enabled (defaults to false) partial lines of text will be rendered to the screen
This commit is contained in:
Michael Davidson 2024-01-20 11:22:12 +11:00
parent e8eabc3cc2
commit 2c52ef30c8
No known key found for this signature in database
GPG key ID: B8D1A99712B8B0EB
3 changed files with 57 additions and 27 deletions

View file

@ -1,4 +1,5 @@
#include "text_run_panel.h" #include "text_run_panel.h"
#include <memory>
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
#include "esphome/components/display/rect.h" #include "esphome/components/display/rect.h"
@ -18,6 +19,7 @@ void TextRunPanel::dump_config(int indent_depth, int additional_level_depth) {
ESP_LOGCONFIG(TAG, "%*sMax Width: %i", indent_depth, "", this->max_width_); ESP_LOGCONFIG(TAG, "%*sMax Width: %i", indent_depth, "", this->max_width_);
ESP_LOGCONFIG(TAG, "%*sText Align: %s", indent_depth, "", ESP_LOGCONFIG(TAG, "%*sText Align: %s", indent_depth, "",
LOG_STR_ARG(display::text_align_to_string(this->text_align_))); LOG_STR_ARG(display::text_align_to_string(this->text_align_)));
ESP_LOGCONFIG(TAG, "%*sDraw Partial Lines: %s", indent_depth, "", YESNO(this->draw_partial_lines_));
ESP_LOGCONFIG(TAG, "%*sText Runs: %i", indent_depth, "", this->text_runs_.size()); ESP_LOGCONFIG(TAG, "%*sText Runs: %i", indent_depth, "", this->text_runs_.size());
for (TextRunBase *run : this->text_runs_) { for (TextRunBase *run : this->text_runs_) {
std::string text = run->get_text(); std::string text = run->get_text();
@ -43,12 +45,23 @@ display::Rect TextRunPanel::measure_item_internal(display::Display *display) {
void TextRunPanel::render_internal(display::Display *display, display::Rect bounds) { void TextRunPanel::render_internal(display::Display *display, display::Rect bounds) {
ESP_LOGD(TAG, "Rendering to (%i, %i)", bounds.w, bounds.h); ESP_LOGD(TAG, "Rendering to (%i, %i)", bounds.w, bounds.h);
CalculatedLayout layout = this->determine_layout_(display, bounds, true); CalculatedLayout layout = this->determine_layout_(display, bounds, false);
int16_t y_offset = 0;
for (const auto &calculated : layout.runs) { display::Point offset = display->get_local_coordinates();
display::Rect clipping_rect = display::Rect(offset.x, offset.y, bounds.w, bounds.h);
display->start_clipping(clipping_rect);
for (const auto &line : layout.lines) {
if (!this->draw_partial_lines_ && ((y_offset + line->max_height) > bounds.h)) {
ESP_LOGD(TAG, "Line %i would partially render outside of the area, skipping", line->line_number);
continue;
}
for (const auto &calculated : line->runs) {
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, calculated->bounds.h, display->filled_rectangle(calculated->bounds.x, calculated->bounds.y, calculated->bounds.w,
calculated->run->background_color_); calculated->bounds.h, 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());
@ -56,10 +69,15 @@ void TextRunPanel::render_internal(display::Display *display, display::Rect boun
if (this->debug_outline_runs_) { if (this->debug_outline_runs_) {
ESP_LOGD(TAG, "Outlining character runs"); ESP_LOGD(TAG, "Outlining character runs");
for (const auto &calculated : layout.runs) { for (const auto &calculated : line->runs) {
display->rectangle(calculated->bounds.x, calculated->bounds.y, calculated->bounds.w, calculated->bounds.h); display->rectangle(calculated->bounds.x, calculated->bounds.y, calculated->bounds.w, calculated->bounds.h);
} }
} }
y_offset += line->max_height;
}
display->end_clipping();
} }
std::vector<std::shared_ptr<CalculatedTextRun>> TextRunPanel::split_runs_into_words_() { std::vector<std::shared_ptr<CalculatedTextRun>> TextRunPanel::split_runs_into_words_() {
@ -95,7 +113,7 @@ std::vector<std::shared_ptr<CalculatedTextRun>> TextRunPanel::split_runs_into_wo
} }
std::vector<std::shared_ptr<LineInfo>> TextRunPanel::fit_words_to_bounds_( std::vector<std::shared_ptr<LineInfo>> TextRunPanel::fit_words_to_bounds_(
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) {
int x_offset = 0; int x_offset = 0;
int y_offset = 0; int y_offset = 0;
int current_line_number = 0; int current_line_number = 0;
@ -114,6 +132,12 @@ std::vector<std::shared_ptr<LineInfo>> TextRunPanel::fit_words_to_bounds_(
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, "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");
break;
}
} }
// Fits on the line // Fits on the line
@ -191,14 +215,13 @@ void TextRunPanel::apply_alignment_to_lines_(std::vector<std::shared_ptr<LineInf
} }
CalculatedLayout TextRunPanel::determine_layout_(display::Display *display, display::Rect bounds, CalculatedLayout TextRunPanel::determine_layout_(display::Display *display, display::Rect bounds,
bool apply_alignment) { bool grow_beyond_bounds_height) {
std::vector<std::shared_ptr<CalculatedTextRun>> runs = this->split_runs_into_words_(); 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); std::vector<std::shared_ptr<LineInfo>> lines = this->fit_words_to_bounds_(runs, bounds, grow_beyond_bounds_height);
this->apply_alignment_to_lines_(lines, this->text_align_); this->apply_alignment_to_lines_(lines, this->text_align_);
CalculatedLayout layout; CalculatedLayout layout;
layout.runs = runs; layout.lines = lines;
layout.line_count = lines.size();
int y_offset = 0; int y_offset = 0;
layout.bounds = display::Rect(0, 0, 0, 0); layout.bounds = display::Rect(0, 0, 0, 0);
@ -208,7 +231,7 @@ CalculatedLayout TextRunPanel::determine_layout_(display::Display *display, disp
} }
layout.bounds.h = y_offset; 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, ESP_LOGD(TAG, "Text fits on %i lines and its bounds are (%i, %i)", layout.lines.size(), layout.bounds.w,
layout.bounds.h); layout.bounds.h);
return layout; return layout;

View file

@ -149,12 +149,6 @@ class CalculatedTextRun {
int16_t baseline{0}; int16_t baseline{0};
}; };
struct CalculatedLayout {
std::vector<std::shared_ptr<CalculatedTextRun>> runs;
display::Rect bounds;
int line_count;
};
class LineInfo { class LineInfo {
public: public:
LineInfo(int line_number) { this->line_number = line_number; } LineInfo(int line_number) { this->line_number = line_number; }
@ -173,6 +167,11 @@ class LineInfo {
int16_t max_baseline{0}; int16_t max_baseline{0};
}; };
struct CalculatedLayout {
std::vector<std::shared_ptr<LineInfo>> lines;
display::Rect bounds;
};
/** 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:
@ -191,13 +190,15 @@ class TextRunPanel : public LayoutItem {
void set_text_align(display::TextAlign text_align) { this->text_align_ = text_align; }; void set_text_align(display::TextAlign text_align) { this->text_align_ = text_align; };
void set_min_width(int min_width) { this->min_width_ = min_width; }; void set_min_width(int min_width) { this->min_width_ = min_width; };
void set_max_width(int max_width) { this->max_width_ = max_width; }; void set_max_width(int max_width) { this->max_width_ = max_width; };
void set_draw_partial_lines(bool draw_partial_lines) { this->draw_partial_lines_ = draw_partial_lines; };
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); CalculatedLayout determine_layout_(display::Display *display, display::Rect bounds, bool grow_beyond_bounds_height);
std::vector<std::shared_ptr<CalculatedTextRun>> split_runs_into_words_(); std::vector<std::shared_ptr<CalculatedTextRun>> split_runs_into_words_();
std::vector<std::shared_ptr<LineInfo>> fit_words_to_bounds_( std::vector<std::shared_ptr<LineInfo>> fit_words_to_bounds_(
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);
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);
std::vector<TextRunBase *> text_runs_; std::vector<TextRunBase *> text_runs_;
@ -205,6 +206,7 @@ class TextRunPanel : public LayoutItem {
int min_width_{0}; int min_width_{0};
int max_width_{0}; int max_width_{0};
TemplatableValue<bool, const CanWrapAtCharacterArguments &> can_wrap_at_character_{}; TemplatableValue<bool, const CanWrapAtCharacterArguments &> can_wrap_at_character_{};
bool draw_partial_lines_{false};
bool debug_outline_runs_{false}; bool debug_outline_runs_{false};
}; };

View file

@ -36,6 +36,7 @@ CONF_TEXT_SENSOR = "text_sensor"
CONF_TEXT_FORMATTER = "text_formatter" CONF_TEXT_FORMATTER = "text_formatter"
CONF_TIME_FORMAT = "time_format" CONF_TIME_FORMAT = "time_format"
CONF_USE_UTC_TIME = "use_utc_time" CONF_USE_UTC_TIME = "use_utc_time"
CONF_DRAW_PARTIAL_LINES = "draw_partial_lines"
TEXT_ALIGN = { TEXT_ALIGN = {
"TOP_LEFT": TextAlign.TOP_LEFT, "TOP_LEFT": TextAlign.TOP_LEFT,
@ -110,6 +111,7 @@ def get_config_schema(base_item_schema, item_type_schema):
cv.Required(CONF_MAX_WIDTH): cv.int_range(min=0), cv.Required(CONF_MAX_WIDTH): cv.int_range(min=0),
cv.Optional(CONF_MIN_WIDTH, default=0): cv.int_range(min=0), cv.Optional(CONF_MIN_WIDTH, default=0): cv.int_range(min=0),
cv.Optional(CONF_CAN_WRAP_AT_CHARACTER): cv.returning_lambda, cv.Optional(CONF_CAN_WRAP_AT_CHARACTER): cv.returning_lambda,
cv.Optional(CONF_DRAW_PARTIAL_LINES, default=False): cv.boolean,
cv.Optional(CONF_DEBUG_OUTLINE_RUNS, default=False): cv.boolean, cv.Optional(CONF_DEBUG_OUTLINE_RUNS, default=False): cv.boolean,
cv.Required(CONF_RUNS): cv.All( cv.Required(CONF_RUNS): cv.All(
cv.ensure_list(RUN_SCHEMA), cv.Length(min=1) cv.ensure_list(RUN_SCHEMA), cv.Length(min=1)
@ -138,6 +140,9 @@ async def config_to_layout_item(pvariable_builder, item_config, child_item_build
) )
cg.add(var.set_can_wrap_at(can_wrap_at_character)) cg.add(var.set_can_wrap_at(can_wrap_at_character))
draw_partial_lines = item_config[CONF_DRAW_PARTIAL_LINES]
cg.add(var.set_draw_partial_lines(draw_partial_lines))
debug_outline_runs = item_config[CONF_DEBUG_OUTLINE_RUNS] debug_outline_runs = item_config[CONF_DEBUG_OUTLINE_RUNS]
if debug_outline_runs: if debug_outline_runs:
cg.add(var.set_debug_outline_runs(debug_outline_runs)) cg.add(var.set_debug_outline_runs(debug_outline_runs))