From ff305147ca920293d0f8e569bb5ff5d84086de29 Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni Date: Thu, 13 Jun 2024 18:43:14 +0200 Subject: [PATCH] Color alpha support. (#884) --- CHANGELOG.md | 5 ++ include/ftxui/screen/color.hpp | 10 +++- include/ftxui/screen/pixel.hpp | 2 +- src/ftxui/dom/clear_under.cpp | 1 + src/ftxui/dom/color.cpp | 31 +++++++++--- src/ftxui/dom/dbox.cpp | 51 +++++++++++++++++++ src/ftxui/screen/color.cpp | 89 +++++++++++++++++++++++++--------- src/ftxui/screen/screen.cpp | 6 ++- 8 files changed, 162 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfe7ad6..a0afeb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,11 @@ current (development) ### Screen - Feature: Add `Box::IsEmpty()`. +- Feature: Color transparency + - Add `Color::RGBA(r,g,b,a)`. + - Add `Color::HSVA(r,g,b,a)`. + - Add `Color::Blend(Color)`. + - Add `Color::IsOpaque()` ### Util - Feature: Support arbitrary `Adapter` for `ConstStringListRef`. See #843. diff --git a/include/ftxui/screen/color.hpp b/include/ftxui/screen/color.hpp index 52f699b..1489dca 100644 --- a/include/ftxui/screen/color.hpp +++ b/include/ftxui/screen/color.hpp @@ -29,10 +29,16 @@ class Color { Color(Palette16 index); // Implicit conversion from index to Color. Color(Palette256 index); // Implicit conversion from index to Color. // NOLINTEND - Color(uint8_t red, uint8_t green, uint8_t blue); + Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha = 255); static Color RGB(uint8_t red, uint8_t green, uint8_t blue); static Color HSV(uint8_t hue, uint8_t saturation, uint8_t value); + static Color RGBA(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha); + static Color HSVA(uint8_t hue, + uint8_t saturation, + uint8_t value, + uint8_t alpha); static Color Interpolate(float t, const Color& a, const Color& b); + static Color Blend(const Color& lhs, const Color& rhs); //--------------------------- // List of colors: @@ -310,6 +316,7 @@ class Color { bool operator!=(const Color& rhs) const; std::string Print(bool is_background_color) const; + bool IsOpaque() const { return alpha_ == 255; } private: enum class ColorType : uint8_t { @@ -322,6 +329,7 @@ class Color { uint8_t red_ = 0; uint8_t green_ = 0; uint8_t blue_ = 0; + uint8_t alpha_ = 0; }; inline namespace literals { diff --git a/include/ftxui/screen/pixel.hpp b/include/ftxui/screen/pixel.hpp index 0a58a38..7545d1e 100644 --- a/include/ftxui/screen/pixel.hpp +++ b/include/ftxui/screen/pixel.hpp @@ -38,7 +38,7 @@ struct Pixel { // The graphemes stored into the pixel. To support combining characters, // like: a?, this can potentially contain multiple codepoints. - std::string character = " "; + std::string character = ""; // Colors: Color background_color = Color::Default; diff --git a/src/ftxui/dom/clear_under.cpp b/src/ftxui/dom/clear_under.cpp index a18510d..81dc2bf 100644 --- a/src/ftxui/dom/clear_under.cpp +++ b/src/ftxui/dom/clear_under.cpp @@ -23,6 +23,7 @@ class ClearUnder : public NodeDecorator { for (int y = box_.y_min; y <= box_.y_max; ++y) { for (int x = box_.x_min; x <= box_.x_max; ++x) { screen.PixelAt(x, y) = Pixel(); + screen.PixelAt(x, y).character = " "; // Consider the pixel written. } } Node::Render(screen); diff --git a/src/ftxui/dom/color.cpp b/src/ftxui/dom/color.cpp index 238ccca..431d522 100644 --- a/src/ftxui/dom/color.cpp +++ b/src/ftxui/dom/color.cpp @@ -19,9 +19,18 @@ class BgColor : public NodeDecorator { : NodeDecorator(std::move(child)), color_(color) {} void Render(Screen& screen) override { - for (int y = box_.y_min; y <= box_.y_max; ++y) { - for (int x = box_.x_min; x <= box_.x_max; ++x) { - screen.PixelAt(x, y).background_color = color_; + if (color_.IsOpaque()) { + for (int y = box_.y_min; y <= box_.y_max; ++y) { + for (int x = box_.x_min; x <= box_.x_max; ++x) { + screen.PixelAt(x, y).background_color = color_; + } + } + } else { + for (int y = box_.y_min; y <= box_.y_max; ++y) { + for (int x = box_.x_min; x <= box_.x_max; ++x) { + Color& color = screen.PixelAt(x, y).background_color; + color = Color::Blend(color, color_); + } } } NodeDecorator::Render(screen); @@ -36,9 +45,18 @@ class FgColor : public NodeDecorator { : NodeDecorator(std::move(child)), color_(color) {} void Render(Screen& screen) override { - for (int y = box_.y_min; y <= box_.y_max; ++y) { - for (int x = box_.x_min; x <= box_.x_max; ++x) { - screen.PixelAt(x, y).foreground_color = color_; + if (color_.IsOpaque()) { + for (int y = box_.y_min; y <= box_.y_max; ++y) { + for (int x = box_.x_min; x <= box_.x_max; ++x) { + screen.PixelAt(x, y).foreground_color = color_; + } + } + } else { + for (int y = box_.y_min; y <= box_.y_max; ++y) { + for (int x = box_.x_min; x <= box_.x_max; ++x) { + Color& color = screen.PixelAt(x, y).foreground_color; + color = Color::Blend(color, color_); + } } } NodeDecorator::Render(screen); @@ -46,6 +64,7 @@ class FgColor : public NodeDecorator { Color color_; }; + } // namespace /// @brief Set the foreground color of an element. diff --git a/src/ftxui/dom/dbox.cpp b/src/ftxui/dom/dbox.cpp index 92971c8..a753215 100644 --- a/src/ftxui/dom/dbox.cpp +++ b/src/ftxui/dom/dbox.cpp @@ -46,6 +46,57 @@ class DBox : public Node { child->SetBox(box); } } + + void Render(Screen& screen) override { + if (children_.size() <= 1) { + return Node::Render(screen); + } + + const int width = box_.x_max - box_.x_min + 1; + const int height = box_.y_max - box_.y_min + 1; + std::vector pixels(size_t(width * height)); + + for (auto& child : children_) { + child->Render(screen); + + // Accumulate the pixels + Pixel* acc = pixels.data(); + for (int x = 0; x < width; ++x) { + for (int y = 0; y < height; ++y) { + auto& pixel = screen.PixelAt(x + box_.x_min, y + box_.y_min); + acc->background_color = + Color::Blend(acc->background_color, pixel.background_color); + acc->automerge = pixel.automerge || acc->automerge; + if (pixel.character == "") { + acc->foreground_color = + Color::Blend(acc->foreground_color, pixel.background_color); + } else { + acc->blink = pixel.blink; + acc->bold = pixel.bold; + acc->dim = pixel.dim; + acc->inverted = pixel.inverted; + acc->underlined = pixel.underlined; + acc->underlined_double = pixel.underlined_double; + acc->strikethrough = pixel.strikethrough; + acc->hyperlink = pixel.hyperlink; + acc->character = pixel.character; + acc->foreground_color = pixel.foreground_color; + } + ++acc; + + pixel = Pixel(); + } + } + } + + // Render the accumulated pixels: + Pixel* acc = pixels.data(); + for (int x = 0; x < width; ++x) { + for (int y = 0; y < height; ++y) { + screen.PixelAt(x + box_.x_min, y + box_.y_min) = *acc++; + } + } + } }; } // namespace diff --git a/src/ftxui/screen/color.cpp b/src/ftxui/screen/color.cpp index a70ec87..546241f 100644 --- a/src/ftxui/screen/color.cpp +++ b/src/ftxui/screen/color.cpp @@ -73,13 +73,15 @@ Color::Color() = default; /// @ingroup screen Color::Color(Palette1 /*value*/) : Color() {} -/// @brief Build a transparent using Palette16 colors. +/// @brief Build a color using the Palette16 colors. /// @ingroup screen -Color::Color(Palette16 index) : type_(ColorType::Palette16), red_(index) {} +Color::Color(Palette16 index) + : type_(ColorType::Palette16), red_(index), alpha_(255) {} -/// @brief Build a transparent using Palette256 colors. +/// @brief Build a color using Palette256 colors. /// @ingroup screen -Color::Color(Palette256 index) : type_(ColorType::Palette256), red_(index) { +Color::Color(Palette256 index) + : type_(ColorType::Palette256), red_(index), alpha_(255) { if (Terminal::ColorSupport() >= Terminal::Color::Palette256) { return; } @@ -93,9 +95,14 @@ Color::Color(Palette256 index) : type_(ColorType::Palette256), red_(index) { /// @param red The quantity of red [0,255] /// @param green The quantity of green [0,255] /// @param blue The quantity of blue [0,255] +/// @param alpha The quantity of alpha [0,255] /// @ingroup screen -Color::Color(uint8_t red, uint8_t green, uint8_t blue) - : type_(ColorType::TrueColor), red_(red), green_(green), blue_(blue) { +Color::Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) + : type_(ColorType::TrueColor), + red_(red), + green_(green), + blue_(blue), + alpha_(alpha) { if (Terminal::ColorSupport() == Terminal::Color::TrueColor) { return; } @@ -136,7 +143,49 @@ Color::Color(uint8_t red, uint8_t green, uint8_t blue) /// @ingroup screen // static Color Color::RGB(uint8_t red, uint8_t green, uint8_t blue) { - return {red, green, blue}; + return RGBA(red, green, blue, 255); +} + +/// @brief Build a Color from its RGBA representation. +/// https://en.wikipedia.org/wiki/RGB_color_model +/// @param red The quantity of red [0,255] +/// @param green The quantity of green [0,255] +/// @param blue The quantity of blue [0,255] +/// @param alpha The quantity of alpha [0,255] +/// @ingroup screen +/// @see Color::RGB +// static +Color Color::RGBA(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) { + return {red, green, blue, alpha}; +} + +/// @brief Build a Color from its HSV representation. +/// https://en.wikipedia.org/wiki/HSL_and_HSV +/// +/// @param h The hue of the color [0,255] +/// @param s The "colorfulness" [0,255]. +/// @param v The "Lightness" [0,255] +/// @param alpha The quantity of alpha [0,255] +/// @ingroup screen +// static +Color Color::HSVA(uint8_t h, uint8_t s, uint8_t v, uint8_t alpha) { + uint8_t region = h / 43; // NOLINT + uint8_t remainder = (h - (region * 43)) * 6; // NOLINT + uint8_t p = (v * (255 - s)) >> 8; // NOLINT + uint8_t q = (v * (255 - ((s * remainder) >> 8))) >> 8; // NOLINT + uint8_t t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8; // NOLINT + + // clang-format off + switch (region) { // NOLINT + case 0: return Color(v,t,p, alpha); // NOLINT + case 1: return Color(q,v,p, alpha); // NOLINT + case 2: return Color(p,v,t, alpha); // NOLINT + case 3: return Color(p,q,v, alpha); // NOLINT + case 4: return Color(t,p,v, alpha); // NOLINT + case 5: return Color(v,p,q, alpha); // NOLINT + } // NOLINT + // clang-format on + return {0, 0, 0, alpha}; } /// @brief Build a Color from its HSV representation. @@ -148,23 +197,7 @@ Color Color::RGB(uint8_t red, uint8_t green, uint8_t blue) { /// @ingroup screen // static Color Color::HSV(uint8_t h, uint8_t s, uint8_t v) { - uint8_t region = h / 43; // NOLINT - uint8_t remainder = (h - (region * 43)) * 6; // NOLINT - uint8_t p = (v * (255 - s)) >> 8; // NOLINT - uint8_t q = (v * (255 - ((s * remainder) >> 8))) >> 8; // NOLINT - uint8_t t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8; // NOLINT - - // clang-format off - switch (region) { // NOLINT - case 0: return Color(v,t,p); // NOLINT - case 1: return Color(q,v,p); // NOLINT - case 2: return Color(p,v,t); // NOLINT - case 3: return Color(p,q,v); // NOLINT - case 4: return Color(t,p,v); // NOLINT - case 5: return Color(v,p,q); // NOLINT - } // NOLINT - // clang-format on - return {0, 0, 0}; + return HSVA(h, s, v, 255); } // static @@ -235,6 +268,14 @@ Color Color::Interpolate(float t, const Color& a, const Color& b) { interp(a_b, b_b)); // } +/// @brief Blend two colors together using the alpha channel. +// static +Color Color::Blend(const Color& lhs, const Color& rhs) { + Color out = Interpolate(float(rhs.alpha_) / 255.F, lhs, rhs); + out.alpha_ = lhs.alpha_ + rhs.alpha_ - lhs.alpha_ * rhs.alpha_ / 255; + return out; +} + inline namespace literals { Color operator""_rgb(unsigned long long int combined) { diff --git a/src/ftxui/screen/screen.cpp b/src/ftxui/screen/screen.cpp index 1a272ba..a04914a 100644 --- a/src/ftxui/screen/screen.cpp +++ b/src/ftxui/screen/screen.cpp @@ -423,7 +423,11 @@ std::string Screen::ToString() const { if (!previous_fullwidth) { UpdatePixelStyle(this, ss, *previous_pixel_ref, pixel); previous_pixel_ref = &pixel; - ss << pixel.character; + if (pixel.character.empty()) { + ss << " "; + } else { + ss << pixel.character; + } } previous_fullwidth = (string_width(pixel.character) == 2); }