diff --git a/.gitignore b/.gitignore index 110437c368..71b66b2499 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ tests/.esphome/ sdkconfig.* !sdkconfig.defaults + +.tests/ \ No newline at end of file diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index cfd73509ea..1b24ca94b9 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -15,6 +15,84 @@ static const char *const TAG = "display"; const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_ON(255, 255, 255, 255); +void Rect::expand(int16_t horizontal, int16_t vertical) { + if ((*this).is_set() && ((*this).w >= (-2 * horizontal)) && ((*this).h >= (-2 * vertical))) { + (*this).x = (*this).x - horizontal; + (*this).y = (*this).y - vertical; + (*this).w = (*this).w + (2 * horizontal); + (*this).h = (*this).h + (2 * vertical); + } +} + +void Rect::extend(Rect rect) { + if (!this->is_set()) { + this->x = rect.x; + this->y = rect.y; + this->w = rect.w; + this->h = rect.h; + } else { + if (this->x > rect.x) { + this->x = rect.x; + } + if (this->y > rect.y) { + this->y = rect.y; + } + if (this->x2() < rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->y2() < rect.y2()) { + this->h = rect.y2() - this->y; + } + } +} +void Rect::shrink(Rect rect) { + if (!this->inside(rect)) { + (*this) = Rect(); + } else { + if (this->x < rect.x) { + this->x = rect.x; + } + if (this->y < rect.y) { + this->y = rect.y; + } + if (this->x2() > rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->y2() > rect.y2()) { + this->h = rect.y2() - this->y; + } + } +} + +bool Rect::inside(int16_t x, int16_t y, bool absolute) { // NOLINT + if (!this->is_set()) { + return true; + } + if (absolute) { + return ((x >= 0) && (x <= this->w) && (y >= 0) && (y <= this->h)); + } else { + return ((x >= this->x) && (x <= this->x2()) && (y >= this->y) && (y <= this->y2())); + } +} + +bool Rect::inside(Rect rect, bool absolute) { + if (!this->is_set() || !rect.is_set()) { + return true; + } + if (absolute) { + return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); + } else { + return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); + } +} + +void Rect::info(const std::string &prefix) { + if (this->is_set()) { + ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d]", prefix.c_str(), this->x, this->y, this->w, this->h); + } else + ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); +} + void DisplayBuffer::init_internal_(uint32_t buffer_length) { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->buffer_ = allocator.allocate(buffer_length); @@ -24,6 +102,7 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) { } this->clear(); } + void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { @@ -50,6 +129,9 @@ int DisplayBuffer::get_height() { } void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; // NOLINT + switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: break; @@ -368,6 +450,10 @@ void DisplayBuffer::do_update_() { } else if (this->writer_.has_value()) { (*this->writer_)(*this); } + // remove all not ended clipping regions + while (is_clipping()) { + end_clipping(); + } } void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) @@ -392,6 +478,41 @@ void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time: } #endif +void DisplayBuffer::start_clipping(Rect rect) { + if (!this->clipping_rectangle_.empty()) { + Rect r = this->clipping_rectangle_.back(); + rect.shrink(r); + } + this->clipping_rectangle_.push_back(rect); +} +void DisplayBuffer::end_clipping() { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "clear: Clipping is not set."); + } else { + this->clipping_rectangle_.pop_back(); + } +} +void DisplayBuffer::extend_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().extend(add_rect); + } +} +void DisplayBuffer::shrink_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().shrink(add_rect); + } +} +Rect DisplayBuffer::get_clipping() { + if (this->clipping_rectangle_.empty()) { + return Rect(); + } else { + return this->clipping_rectangle_.back(); + } +} bool Glyph::get_pixel(int x, int y) const { const int x_data = x - this->glyph_data_->offset_x; const int y_data = y - this->glyph_data_->offset_y; diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index b652989067..03d64f80d3 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -4,7 +4,6 @@ #include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "display_color_utils.h" - #include #include @@ -100,6 +99,32 @@ enum DisplayRotation { DISPLAY_ROTATION_270_DEGREES = 270, }; +static const int16_t VALUE_NO_SET = 32766; + +class Rect { + public: + int16_t x; ///< X coordinate of corner + int16_t y; ///< Y coordinate of corner + int16_t w; ///< Width of region + int16_t h; ///< Height of region + + Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT + inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {} + inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner + inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner + + inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); } + + void expand(int16_t horizontal, int16_t vertical); + + void extend(Rect rect); + void shrink(Rect rect); + + bool inside(Rect rect, bool absolute = false); + bool inside(int16_t x, int16_t y, bool absolute = false); + void info(const std::string &prefix = "rect info:"); +}; + class Font; class Image; class DisplayBuffer; @@ -126,6 +151,7 @@ class DisplayBuffer { int get_width(); /// Get the height of the image in pixels with rotation applied. int get_height(); + /// Set a single pixel at the specified coordinates to the given color. void draw_pixel_at(int x, int y, Color color = COLOR_ON); @@ -374,6 +400,61 @@ class DisplayBuffer { */ virtual DisplayType get_display_type() = 0; + /// + /// Set the clipping rectangle for further drawing + /// + /// \param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) + /// + /// \return true if success, false if error + /// + void start_clipping(Rect rect); + void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + start_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /// + /// Add a rectangular region to the invalidation region + /// - This is usually called when an element has been modified + /// + /// \param[in] rect: Rectangle to add to the invalidation region + /// + /// \return none + /// + void extend_clipping(Rect rect); + void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + this->extend_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /// + /// substract a rectangular region to the invalidation region + /// - This is usually called when an element has been modified + /// + /// \param[in] rect: Rectangle to add to the invalidation region + /// + /// \return none + /// + void shrink_clipping(Rect rect); + void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { + this->shrink_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /// + /// Reset the invalidation region + /// + /// \return none + /// + void end_clipping(); + + /// + /// Get the current the clipping rectangle + /// + /// + /// \return rect for active clipping region + /// + Rect get_clipping(); + + bool is_clipping() const { return this->clipping_rectangle_.empty(); } + protected: void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); @@ -390,6 +471,7 @@ class DisplayBuffer { DisplayPage *previous_page_{nullptr}; std::vector on_page_change_triggers_; bool auto_clear_enabled_{true}; + std::vector clipping_rectangle_; }; class DisplayPage {