From e83e90ced29c4f06a4cd4fa6a42d840b6727192c Mon Sep 17 00:00:00 2001 From: Vinicius Moura Longaray Date: Wed, 22 Mar 2023 09:59:02 -0300 Subject: [PATCH] Feature: `LinearGradient` color decorator. (#592) Based on the existing color decorators, create new ones to apply a gradient effect on the DOM. Co-authored-by: ArthurSonzogni --- CHANGELOG.md | 4 + CMakeLists.txt | 1 + cmake/ftxui_test.cmake | 1 + doc/mainpage.md | 35 ++- examples/component/CMakeLists.txt | 1 + .../component/linear_gradient_gallery.cpp | 56 ++++ examples/dom/CMakeLists.txt | 3 +- examples/dom/linear_gradient.cpp | 26 ++ examples/dom/style_color.cpp | 103 +++--- include/ftxui/dom/elements.hpp | 5 + include/ftxui/dom/linear_gradient.hpp | 52 ++++ include/ftxui/screen/color.hpp | 3 +- src/ftxui/component/button_test.cpp | 48 +-- src/ftxui/dom/border.cpp | 17 +- src/ftxui/dom/color_test.cpp | 4 +- src/ftxui/dom/linear_gradient.cpp | 293 ++++++++++++++++++ src/ftxui/dom/linear_gradient_test.cpp | 90 ++++++ src/ftxui/dom/table.cpp | 12 +- src/ftxui/screen/color.cpp | 15 +- src/ftxui/screen/color_test.cpp | 8 +- 20 files changed, 671 insertions(+), 106 deletions(-) create mode 100644 examples/component/linear_gradient_gallery.cpp create mode 100644 examples/dom/linear_gradient.cpp create mode 100644 include/ftxui/dom/linear_gradient.hpp create mode 100644 src/ftxui/dom/linear_gradient.cpp create mode 100644 src/ftxui/dom/linear_gradient_test.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5955e..39ef3c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ current (development) ### Dom - Feature: Add the dashed style for border and separator. - Feature: Add colored borders. +- Feature: Customize with gradient color effect. Add the following decorators: + - `colorgrad` + - `bgcolorgrad` +- Improvement: Color::Interpolate() uses gamma correction. ### - Breaking: Direction enum is renamed WidthOrHeight diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f28c45..c52c339 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,7 @@ add_library(dom src/ftxui/dom/gridbox.cpp src/ftxui/dom/hbox.cpp src/ftxui/dom/inverted.cpp + src/ftxui/dom/linear_gradient.cpp src/ftxui/dom/node.cpp src/ftxui/dom/node_decorator.cpp src/ftxui/dom/paragraph.cpp diff --git a/cmake/ftxui_test.cmake b/cmake/ftxui_test.cmake index 996e5c3..456fcb8 100644 --- a/cmake/ftxui_test.cmake +++ b/cmake/ftxui_test.cmake @@ -37,6 +37,7 @@ add_executable(ftxui-tests src/ftxui/dom/gauge_test.cpp src/ftxui/dom/gridbox_test.cpp src/ftxui/dom/hbox_test.cpp + src/ftxui/dom/linear_gradient_test.cpp src/ftxui/dom/scroll_indicator_test.cpp src/ftxui/dom/separator_test.cpp src/ftxui/dom/spinner_test.cpp diff --git a/doc/mainpage.md b/doc/mainpage.md index 40b9103..6ac8572 100644 --- a/doc/mainpage.md +++ b/doc/mainpage.md @@ -442,6 +442,36 @@ ftxui::Color::HSV(uint8_t hue, uint8_t saturation, uint8_t value); @endhtmlonly +## LinearGradient #{#dom-linear-gradient} + +FTXUI supports linear gradient. Either on the foreground or the background. + +```cpp +Decorator color(const LinearGradient&); +Decorator bgcolor(const LinearGradient&); +``` + +A `ftxui::LinearGradient` is defined by an angle in degree, and a list of color +stops. +```cpp +auto gradient = LinearGradient() + .Angle(45) + .AddStop(0.0, Color::Red) + .AddStop(0.5, Color::Green) + .AddStop(1.0, Color::Blue); +``` + +You can also use simplified constructors: +```cpp +LinearGradient(Color::Red, Color::Blue); +``` +```cpp +LinearGradient(45, Color::Red, Color::Blue); +``` + +See [demo](https://arthursonzogni.github.io/FTXUI/examples/?file=component/linear_gradient_gallery). + + ## Style {#dom-style} In addition to colored text and colored backgrounds. Many terminals support text effects such as: `bold`, `dim`, `underlined`, `inverted`, `blink`. @@ -456,6 +486,8 @@ Element strikethrough(Element); Element blink(Element); Decorator color(Color); Decorator bgcolor(Color); +Decorator colorgrad(LinearGradient); +Decorator bgcolorgrad(LinearGradient); ``` [Example](https://arthursonzogni.github.io/FTXUI/examples_2dom_2style_gallery_8cpp-example.html) @@ -560,8 +592,7 @@ Simple [example](https://github.com/ArthurSonzogni/FTXUI/blob/master/examples/do Complex [example](https://github.com/ArthurSonzogni/FTXUI/blob/master/examples/component/canvas_animated.cpp): -![ezgif com-gif-maker (3)](https://user-images.githubusercontent.com/4759106/147250538-783a8246-98e0-4a25-b032-3bd3710549d1.gif) - +![ezgif com-gif-maker (3)](https://user-images.githubusercontent.com/4759106/147250538-783a8246-98e0-4a25-b032-3bd3710549d1.gif) # component {#module-component} The `ftxui::component` module defines the logic that produces interactive diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt index 5b618d2..c080690 100644 --- a/examples/component/CMakeLists.txt +++ b/examples/component/CMakeLists.txt @@ -17,6 +17,7 @@ example(focus_cursor) example(gallery) example(homescreen) example(input) +example(linear_gradient_gallery) example(maybe) example(menu) example(menu2) diff --git a/examples/component/linear_gradient_gallery.cpp b/examples/component/linear_gradient_gallery.cpp new file mode 100644 index 0000000..e0368c8 --- /dev/null +++ b/examples/component/linear_gradient_gallery.cpp @@ -0,0 +1,56 @@ +#include // for ComponentBase, Component +#include // for operator|, Element, flex, bgcolor, text, vbox, center +#include // for LinearGradient +#include // for Color, Color::Blue, Color::Red +#include // for __shared_ptr_access, shared_ptr +#include // for allocator, operator+, char_traits, string, to_string + +#include "ftxui/component/captured_mouse.hpp" // for ftxui +#include "ftxui/component/component.hpp" // for Slider, Renderer, Vertical +#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive + +int main(int argc, const char* argv[]) { + using namespace ftxui; + auto screen = ScreenInteractive::Fullscreen(); + + int angle = 180.f; + float start = 0.f; + float end = 1.f; + + std::string slider_angle_text; + std::string slider_start_text; + std::string slider_end_text; + + auto slider_angle = Slider(&slider_angle_text, &angle, 0, 360); + auto slider_start = Slider(&slider_start_text, &start, 0.f, 1.f); + auto slider_end = Slider(&slider_end_text, &end, 0.f, 1.f); + + auto layout = Container::Vertical({ + slider_angle, + slider_start, + slider_end, + }); + + auto renderer = Renderer(layout, [&] { + slider_angle_text = "angle = " + std::to_string(angle) + "°"; + slider_start_text = "start = " + std::to_string(int(start * 100)) + "%"; + slider_end_text = "end = " + std::to_string(int(end * 100)) + "%"; + + auto background = text("Gradient") | center | + bgcolor(LinearGradient() + .Angle(angle) + .Stop(Color::Blue, start) + .Stop(Color::Red, end)); + return vbox({ + background | flex, + layout->Render(), + }) | + flex; + }); + + screen.Loop(renderer); +} + +// Copyright 2023 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/examples/dom/CMakeLists.txt b/examples/dom/CMakeLists.txt index 7923536..ad3285e 100644 --- a/examples/dom/CMakeLists.txt +++ b/examples/dom/CMakeLists.txt @@ -3,18 +3,19 @@ set(DIRECTORY_LIB dom) example(border) example(border_colored) example(border_style) +example(canvas) example(color_gallery) example(color_info_palette256) example(color_truecolor_HSV) example(color_truecolor_RGB) example(dbox) -example(canvas) example(gauge) example(gauge_direction) example(graph) example(gridbox) example(hflow) example(html_like) +example(linear_gradient) example(package_manager) example(paragraph) example(separator) diff --git a/examples/dom/linear_gradient.cpp b/examples/dom/linear_gradient.cpp new file mode 100644 index 0000000..affc370 --- /dev/null +++ b/examples/dom/linear_gradient.cpp @@ -0,0 +1,26 @@ +#include // for bgcolor, operator|, operator|=, text, center, Element +#include // for LinearGradient::Stop, LinearGradient +#include // for Full, Screen +#include // for allocator, shared_ptr + +#include "ftxui/dom/node.hpp" // for Render +#include "ftxui/screen/color.hpp" // for Color, Color::DeepPink1, Color::DeepSkyBlue1, Color::Yellow, ftxui + +int main(int argc, const char* argv[]) { + using namespace ftxui; + auto document = text("gradient") | center; + + document |= bgcolor(LinearGradient() + .Angle(45) + .Stop(Color::DeepPink1) + .Stop(Color::DeepSkyBlue1)); + auto screen = Screen::Create(Dimension::Full(), Dimension::Full()); + Render(screen, document); + screen.Print(); + + return 0; +} + +// Copyright 2023 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/examples/dom/style_color.cpp b/examples/dom/style_color.cpp index 6f42bd1..de4a293 100644 --- a/examples/dom/style_color.cpp +++ b/examples/dom/style_color.cpp @@ -1,59 +1,60 @@ -#include // for Full, Screen -#include // for allocator +#include // for LinearGradient +#include // for Full, Screen +#include // for allocator -#include "ftxui/dom/elements.hpp" // for text, bgcolor, color, vbox, Fit, filler, hbox +#include "ftxui/dom/elements.hpp" // for text, bgcolor, color, vbox, filler, Fit, hbox #include "ftxui/dom/node.hpp" // for Render -#include "ftxui/screen/box.hpp" // for ftxui -#include "ftxui/screen/color.hpp" // for Color, Color::Black, Color::Blue, Color::BlueLight, Color::Cyan, Color::CyanLight, Color::Default, Color::GrayDark, Color::GrayLight, Color::Green, Color::GreenLight, Color::Magenta, Color::MagentaLight, Color::Red, Color::RedLight, Color::White, Color::Yellow, Color::YellowLight +#include "ftxui/screen/color.hpp" // for Color, operator""_rgb, Color::Black, Color::Blue, Color::BlueLight, Color::Cyan, Color::CyanLight, Color::DeepSkyBlue4, Color::Default, Color::GrayDark, Color::GrayLight, Color::Green, Color::GreenLight, Color::Magenta, Color::MagentaLight, Color::Red, Color::RedLight, Color::SkyBlue1, Color::White, Color::Yellow, Color::YellowLight, ftxui int main(int argc, const char* argv[]) { using namespace ftxui; - // clang-format off - auto document = - hbox( - vbox( - color(Color::Default, text("Default")), - color(Color::Black, text("Black")), - color(Color::GrayDark, text("GrayDark")), - color(Color::GrayLight, text("GrayLight")), - color(Color::White, text("White")), - color(Color::Blue, text("Blue")), - color(Color::BlueLight, text("BlueLight")), - color(Color::Cyan, text("Cyan")), - color(Color::CyanLight, text("CyanLight")), - color(Color::Green, text("Green")), - color(Color::GreenLight, text("GreenLight")), - color(Color::Magenta, text("Magenta")), - color(Color::MagentaLight, text("MagentaLight")), - color(Color::Red, text("Red")), - color(Color::RedLight, text("RedLight")), - color(Color::Yellow, text("Yellow")), - color(Color::YellowLight, text("YellowLight")), - color(0x66ff66_rgb, text("Phosphor")) - ), - vbox( - bgcolor(Color::Default, text("Default")), - bgcolor(Color::Black, text("Black")), - bgcolor(Color::GrayDark, text("GrayDark")), - bgcolor(Color::GrayLight, text("GrayLight")), - bgcolor(Color::White, text("White")), - bgcolor(Color::Blue, text("Blue")), - bgcolor(Color::BlueLight, text("BlueLight")), - bgcolor(Color::Cyan, text("Cyan")), - bgcolor(Color::CyanLight, text("CyanLight")), - bgcolor(Color::Green, text("Green")), - bgcolor(Color::GreenLight, text("GreenLight")), - bgcolor(Color::Magenta, text("Magenta")), - bgcolor(Color::MagentaLight, text("MagentaLight")), - bgcolor(Color::Red, text("Red")), - bgcolor(Color::RedLight, text("RedLight")), - bgcolor(Color::Yellow, text("Yellow")), - bgcolor(Color::YellowLight, text("YellowLight")), - bgcolor(0x66ff66_rgb, text("Phosphor")) - ), - filler() - ); - // clang-format on + auto document = hbox({ + vbox({ + color(Color::Default, text("Default")), + color(Color::Black, text("Black")), + color(Color::GrayDark, text("GrayDark")), + color(Color::GrayLight, text("GrayLight")), + color(Color::White, text("White")), + color(Color::Blue, text("Blue")), + color(Color::BlueLight, text("BlueLight")), + color(Color::Cyan, text("Cyan")), + color(Color::CyanLight, text("CyanLight")), + color(Color::Green, text("Green")), + color(Color::GreenLight, text("GreenLight")), + color(Color::Magenta, text("Magenta")), + color(Color::MagentaLight, text("MagentaLight")), + color(Color::Red, text("Red")), + color(Color::RedLight, text("RedLight")), + color(Color::Yellow, text("Yellow")), + color(Color::YellowLight, text("YellowLight")), + color(0x66ff66_rgb, text("Phosphor")), + color(LinearGradient(Color::SkyBlue1, Color::DeepSkyBlue4), + text("Skyblue to DeepSkyBlue")), + }), + vbox({ + bgcolor(Color::Default, text("Default")), + bgcolor(Color::Black, text("Black")), + bgcolor(Color::GrayDark, text("GrayDark")), + bgcolor(Color::GrayLight, text("GrayLight")), + bgcolor(Color::White, text("White")), + bgcolor(Color::Blue, text("Blue")), + bgcolor(Color::BlueLight, text("BlueLight")), + bgcolor(Color::Cyan, text("Cyan")), + bgcolor(Color::CyanLight, text("CyanLight")), + bgcolor(Color::Green, text("Green")), + bgcolor(Color::GreenLight, text("GreenLight")), + bgcolor(Color::Magenta, text("Magenta")), + bgcolor(Color::MagentaLight, text("MagentaLight")), + bgcolor(Color::Red, text("Red")), + bgcolor(Color::RedLight, text("RedLight")), + bgcolor(Color::Yellow, text("Yellow")), + bgcolor(Color::YellowLight, text("YellowLight")), + bgcolor(0x66ff66_rgb, text("Phosphor")), + bgcolor(LinearGradient(Color::SkyBlue1, Color::DeepSkyBlue4), + text("Skyblue to DeepSkyBlue")), + }), + filler(), + }); auto screen = Screen::Create(Dimension::Full(), Dimension::Fit(document)); Render(screen, document); diff --git a/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp index 2b615a2..b414c76 100644 --- a/include/ftxui/dom/elements.hpp +++ b/include/ftxui/dom/elements.hpp @@ -7,6 +7,7 @@ #include "ftxui/dom/canvas.hpp" #include "ftxui/dom/direction.hpp" #include "ftxui/dom/flexbox_config.hpp" +#include "ftxui/dom/linear_gradient.hpp" #include "ftxui/dom/node.hpp" #include "ftxui/screen/box.hpp" #include "ftxui/screen/color.hpp" @@ -99,8 +100,12 @@ Element blink(Element); Element strikethrough(Element); Decorator color(Color); Decorator bgcolor(Color); +Decorator color(const LinearGradient&); +Decorator bgcolor(const LinearGradient&); Element color(Color, Element); Element bgcolor(Color, Element); +Element color(const LinearGradient&, Element); +Element bgcolor(const LinearGradient&, Element); Decorator focusPosition(int x, int y); Decorator focusPositionRelative(float x, float y); Element automerge(Element child); diff --git a/include/ftxui/dom/linear_gradient.hpp b/include/ftxui/dom/linear_gradient.hpp new file mode 100644 index 0000000..3c6b529 --- /dev/null +++ b/include/ftxui/dom/linear_gradient.hpp @@ -0,0 +1,52 @@ +#ifndef FTXUI_DOM_LINEAR_GRADIENT_HPP +#define FTXUI_DOM_LINEAR_GRADIENT_HPP + +#include +#include + +#include "ftxui/screen/color.hpp" // for Colors + +namespace ftxui { + +/// @brief A class representing the settings for linear-gradient color effect. +/// +/// Example: +/// ```cpp +/// LinearGradient() +/// .Angle(45) +/// .Stop(Color::Red, 0.0) +/// .Stop(Color::Green, 0.5) +/// .Stop(Color::Blue, 1.0); +/// ``` +/// +/// There are also shorthand constructors: +/// ```cpp +/// LinearGradient(Color::Red, Color::Blue); +/// LinearGradient(45, Color::Red, Color::Blue); +/// ``` +struct LinearGradient { + float angle = 0.f; + struct Stop { + Color color = Color::Default; + std::optional position; + }; + std::vector stops; + + // Simple constructor + LinearGradient(); + LinearGradient(Color begin, Color end); + LinearGradient(float angle, Color begin, Color end); + + // Modifier using the builder pattern. + LinearGradient& Angle(float angle); + LinearGradient& Stop(Color color, float position); + LinearGradient& Stop(Color color); +}; + +} // namespace ftxui + +#endif // FTXUI_DOM_LINEAR_GRADIENT_HPP + +// Copyright 2023 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/include/ftxui/screen/color.hpp b/include/ftxui/screen/color.hpp index d7d789a..6adf1b3 100644 --- a/include/ftxui/screen/color.hpp +++ b/include/ftxui/screen/color.hpp @@ -2,7 +2,8 @@ #define FTXUI_SCREEN_COLOR_HPP #include // for uint8_t -#include // for wstring +#include // for string +#include // for vector #ifdef RGB // Workaround for wingdi.h (via Windows.h) defining macros that break things. diff --git a/src/ftxui/component/button_test.cpp b/src/ftxui/component/button_test.cpp index 8f7add6..44c44aa 100644 --- a/src/ftxui/component/button_test.cpp +++ b/src/ftxui/component/button_test.cpp @@ -124,10 +124,10 @@ TEST(ButtonTest, Animation) { Screen screen(12, 3); Render(screen, container->Render()); EXPECT_EQ(screen.ToString(), - "\x1B[1m\x1B[38;2;192;192;192m\x1B[48;2;0;0;0m \x1B[22m " - " \x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;192;192;192m\x1B[48;2;0;0;" + "\x1B[1m\x1B[38;2;191;191;191m\x1B[48;2;0;0;0m \x1B[22m " + " \x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;191;191;191m\x1B[48;2;0;0;" "0m btn1 \x1B[22m btn2 " - "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;192;192;192m\x1B[48;2;0;0;" + "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;191;191;191m\x1B[48;2;0;0;" "0m \x1B[22m \x1B[39m\x1B[49m"); } selected = 1; @@ -135,10 +135,10 @@ TEST(ButtonTest, Animation) { Screen screen(12, 3); Render(screen, container->Render()); EXPECT_EQ(screen.ToString(), - "\x1B[38;2;192;192;192m\x1B[48;2;0;0;0m \x1B[1m " - "\x1B[22m\x1B[39m\x1B[49m\r\n\x1B[38;2;192;192;192m\x1B[48;2;0;0;" + "\x1B[38;2;191;191;191m\x1B[48;2;0;0;0m \x1B[1m " + "\x1B[22m\x1B[39m\x1B[49m\r\n\x1B[38;2;191;191;191m\x1B[48;2;0;0;" "0m btn1 \x1B[1m btn2 " - "\x1B[22m\x1B[39m\x1B[49m\r\n\x1B[38;2;192;192;192m\x1B[48;2;0;0;" + "\x1B[22m\x1B[39m\x1B[49m\r\n\x1B[38;2;191;191;191m\x1B[48;2;0;0;" "0m \x1B[1m \x1B[22m\x1B[39m\x1B[49m"); } animation::Params params(2s); @@ -148,12 +148,12 @@ TEST(ButtonTest, Animation) { Render(screen, container->Render()); EXPECT_EQ( screen.ToString(), - "\x1B[38;2;192;192;192m\x1B[48;2;0;0;0m " - "\x1B[1m\x1B[38;2;255;255;255m\x1B[48;2;128;128;128m " - "\x1B[22m\x1B[39m\x1B[49m\r\n\x1B[38;2;192;192;192m\x1B[48;2;0;0;0m " - "btn1 \x1B[1m\x1B[38;2;255;255;255m\x1B[48;2;128;128;128m btn2 " - "\x1B[22m\x1B[39m\x1B[49m\r\n\x1B[38;2;192;192;192m\x1B[48;2;0;0;0m " - " \x1B[1m\x1B[38;2;255;255;255m\x1B[48;2;128;128;128m " + "\x1B[38;2;191;191;191m\x1B[48;2;0;0;0m " + "\x1B[1m\x1B[38;2;254;254;254m\x1B[48;2;127;127;127m " + "\x1B[22m\x1B[39m\x1B[49m\r\n\x1B[38;2;191;191;191m\x1B[48;2;0;0;0m " + "btn1 \x1B[1m\x1B[38;2;254;254;254m\x1B[48;2;127;127;127m btn2 " + "\x1B[22m\x1B[39m\x1B[49m\r\n\x1B[38;2;191;191;191m\x1B[48;2;0;0;0m " + " \x1B[1m\x1B[38;2;254;254;254m\x1B[48;2;127;127;127m " "\x1B[22m\x1B[39m\x1B[49m"); } EXPECT_EQ(selected, 1); @@ -164,12 +164,12 @@ TEST(ButtonTest, Animation) { Render(screen, container->Render()); EXPECT_EQ( screen.ToString(), - "\x1B[1m\x1B[38;2;223;223;223m\x1B[48;2;64;64;64m " - "\x1B[22m\x1B[38;2;255;255;255m\x1B[48;2;128;128;128m " - "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;223;223;223m\x1B[48;2;64;64;64m " - "btn1 \x1B[22m\x1B[38;2;255;255;255m\x1B[48;2;128;128;128m btn2 " - "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;223;223;223m\x1B[48;2;64;64;64m " - " \x1B[22m\x1B[38;2;255;255;255m\x1B[48;2;128;128;128m " + "\x1B[1m\x1B[38;2;226;226;226m\x1B[48;2;93;93;93m " + "\x1B[22m\x1B[38;2;254;254;254m\x1B[48;2;127;127;127m " + "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;226;226;226m\x1B[48;2;93;93;93m " + "btn1 \x1B[22m\x1B[38;2;254;254;254m\x1B[48;2;127;127;127m btn2 " + "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;226;226;226m\x1B[48;2;93;93;93m " + " \x1B[22m\x1B[38;2;254;254;254m\x1B[48;2;127;127;127m " "\x1B[39m\x1B[49m"); } container->OnAnimation(params); @@ -178,12 +178,12 @@ TEST(ButtonTest, Animation) { Render(screen, container->Render()); EXPECT_EQ( screen.ToString(), - "\x1B[1m\x1B[38;2;255;255;255m\x1B[48;2;128;128;128m " - "\x1B[22m\x1B[38;2;192;192;192m\x1B[48;2;0;0;0m " - "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;255;255;255m\x1B[48;2;128;128;" - "128m btn1 \x1B[22m\x1B[38;2;192;192;192m\x1B[48;2;0;0;0m btn2 " - "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;255;255;255m\x1B[48;2;128;128;" - "128m \x1B[22m\x1B[38;2;192;192;192m\x1B[48;2;0;0;0m " + "\x1B[1m\x1B[38;2;254;254;254m\x1B[48;2;127;127;127m " + "\x1B[22m\x1B[38;2;191;191;191m\x1B[48;2;0;0;0m " + "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;254;254;254m\x1B[48;2;127;127;" + "127m btn1 \x1B[22m\x1B[38;2;191;191;191m\x1B[48;2;0;0;0m btn2 " + "\x1B[39m\x1B[49m\r\n\x1B[1m\x1B[38;2;254;254;254m\x1B[48;2;127;127;" + "127m \x1B[22m\x1B[38;2;191;191;191m\x1B[48;2;0;0;0m " "\x1B[39m\x1B[49m"); } } diff --git a/src/ftxui/dom/border.cpp b/src/ftxui/dom/border.cpp index a90554b..6b2a63b 100644 --- a/src/ftxui/dom/border.cpp +++ b/src/ftxui/dom/border.cpp @@ -1,12 +1,13 @@ -#include // for max -#include // for array -#include // for allocator, make_shared, __shared_ptr_access -#include -#include // for basic_string, string -#include // for move -#include // for __alloc_traits<>::value_type +#include // for max +#include // for array +#include // for Color +#include // for allocator, make_shared, __shared_ptr_access +#include // for optional, nullopt +#include // for basic_string, string +#include // for move +#include // for __alloc_traits<>::value_type -#include "ftxui/dom/elements.hpp" // for unpack, Element, Decorator, BorderStyle, ROUNDED, Elements, DOUBLE, EMPTY, HEAVY, LIGHT, border, borderDouble, borderEmpty, borderHeavy, borderLight, borderRounded, borderStyled, borderWith, window +#include "ftxui/dom/elements.hpp" // for unpack, Element, Decorator, BorderStyle, ROUNDED, borderStyled, Elements, DASHED, DOUBLE, EMPTY, HEAVY, LIGHT, border, borderDashed, borderDouble, borderEmpty, borderHeavy, borderLight, borderRounded, borderWith, window #include "ftxui/dom/node.hpp" // for Node, Elements #include "ftxui/dom/requirement.hpp" // for Requirement #include "ftxui/screen/box.hpp" // for Box diff --git a/src/ftxui/dom/color_test.cpp b/src/ftxui/dom/color_test.cpp index 0ff72c3..3ff2d18 100644 --- a/src/ftxui/dom/color_test.cpp +++ b/src/ftxui/dom/color_test.cpp @@ -1,9 +1,9 @@ -#include +#include // for Test, EXPECT_EQ, Message, TestPartResult, TestInfo (ptr only), TEST #include // for allocator #include "ftxui/dom/elements.hpp" // for operator|, text, bgcolor, color, Element #include "ftxui/dom/node.hpp" // for Render -#include "ftxui/screen/color.hpp" // for Color, Color::Red +#include "ftxui/screen/color.hpp" // for Color, Color::Red, Color::RedLight #include "ftxui/screen/screen.hpp" // for Screen, Pixel namespace ftxui { diff --git a/src/ftxui/dom/linear_gradient.cpp b/src/ftxui/dom/linear_gradient.cpp new file mode 100644 index 0000000..39046a3 --- /dev/null +++ b/src/ftxui/dom/linear_gradient.cpp @@ -0,0 +1,293 @@ +#include // for size_t +#include // for max, min, sort, copy +#include // for fmod, cos, sin +#include // for LinearGradient::Stop, LinearGradient +#include // for allocator_traits<>::value_type, make_shared +#include // for optional, operator!=, operator< +#include // for move +#include // for vector + +#include "ftxui/dom/elements.hpp" // for Element, Decorator, bgcolor, color +#include "ftxui/dom/node_decorator.hpp" // for NodeDecorator +#include "ftxui/screen/box.hpp" // for Box +#include "ftxui/screen/color.hpp" // for Color, Color::Default, Color::Blue +#include "ftxui/screen/screen.hpp" // for Pixel, Screen + +namespace ftxui { +namespace { + +struct LinearGradientNormalized { + float angle = 0.f; + std::vector colors; + std::vector positions; // Sorted. +}; + +// Convert a LinearGradient to a normalized version. +LinearGradientNormalized Normalize(LinearGradient gradient) { + // Handle gradient of size 0. + if (gradient.stops.size() == 0) { + return LinearGradientNormalized{ + 0.f, {Color::Default, Color::Default}, {0.f, 1.f}}; + } + + // Fill in the two extent, if not provided. + if (!gradient.stops.front().position) { + gradient.stops.front().position = 0; + } + if (!gradient.stops.back().position) { + gradient.stops.back().position = 1.f; + } + + // Fill in the blank, by interpolating positions. + size_t last_checkpoint = 0; + for (size_t i = 1; i < gradient.stops.size(); ++i) { + if (!gradient.stops[i].position) { + continue; + } + + if (i - last_checkpoint >= 2) { + const float min = gradient.stops[i].position.value(); + const float max = gradient.stops[last_checkpoint].position.value(); + for (size_t j = last_checkpoint + 1; j < i; ++j) { + gradient.stops[j].position = + min + (max - min) * (j - last_checkpoint) / (i - last_checkpoint); + } + } + + last_checkpoint = i; + } + + // Sort the stops by position. + std::sort( + gradient.stops.begin(), gradient.stops.end(), + [](const auto& a, const auto& b) { return a.position < b.position; }); + + // If we don't being with zero, add a stop at zero. + if (gradient.stops.front().position != 0) { + gradient.stops.insert(gradient.stops.begin(), + {gradient.stops.front().color, 0.f}); + } + // If we don't end with one, add a stop at one. + if (gradient.stops.back().position != 1) { + gradient.stops.push_back({gradient.stops.back().color, 1.f}); + } + + // Normalize the angle. + LinearGradientNormalized normalized; + normalized.angle = std::fmod(std::fmod(gradient.angle, 360.f) + 360.f, 360.f); + for (auto& stop : gradient.stops) { + normalized.colors.push_back(stop.color); + normalized.positions.push_back(stop.position.value()); + } + return normalized; +} + +Color Interpolate(const LinearGradientNormalized& gradient, float t) { + // Find the right color in the gradient's stops. + size_t i = 1; + while (true) { + if (i > gradient.positions.size()) { + return Color::Interpolate(0.5f, gradient.colors.back(), + gradient.colors.back()); + } + if (t <= gradient.positions[i]) { + break; + } + ++i; + } + + const float t0 = gradient.positions[i - 1]; + const float t1 = gradient.positions[i - 0]; + const float tt = (t - t0) / (t1 - t0); + + const Color& c0 = gradient.colors[i - 1]; + const Color& c1 = gradient.colors[i - 0]; + const Color& cc = Color::Interpolate(tt, c0, c1); + + return cc; +} + +class LinearGradientColor : public NodeDecorator { + public: + explicit LinearGradientColor(Element child, + const LinearGradient& gradient, + bool background_color) + : NodeDecorator(std::move(child)), + gradient_(Normalize(gradient)), + background_color_{background_color} {} + + private: + void Render(Screen& screen) override { + const float degtorad = 0.01745329251f; + const float dx = std::cos(gradient_.angle * degtorad); + const float dy = std::sin(gradient_.angle * degtorad); + + // Project every corner to get the extent of the gradient. + const float p1 = box_.x_min * dx + box_.y_min * dy; + const float p2 = box_.x_min * dx + box_.y_max * dy; + const float p3 = box_.x_max * dx + box_.y_min * dy; + const float p4 = box_.x_max * dx + box_.y_max * dy; + const float min = std::min({p1, p2, p3, p4}); + const float max = std::max({p1, p2, p3, p4}); + + // Renormalize the projection to [0, 1] using the extent and projective + // geometry. + const float dX = dx / (max - min); + const float dY = dy / (max - min); + const float dZ = -min / (max - min); + + // Project every pixel to get the color. + if (background_color_) { + for (int y = box_.y_min; y <= box_.y_max; ++y) { + for (int x = box_.x_min; x <= box_.x_max; ++x) { + const float t = x * dX + y * dY + dZ; + screen.PixelAt(x, y).background_color = Interpolate(gradient_, t); + } + } + } else { + for (int y = box_.y_min; y <= box_.y_max; ++y) { + for (int x = box_.x_min; x <= box_.x_max; ++x) { + const float t = x * dX + y * dY + dZ; + screen.PixelAt(x, y).foreground_color = Interpolate(gradient_, t); + } + } + } + + NodeDecorator::Render(screen); + } + + LinearGradientNormalized gradient_; + bool background_color_; +}; + +} // namespace + +/// @brief Build the "empty" gradient. This is often followed by calls to +/// LinearGradient::Angle() and LinearGradient::Stop(). +/// Example: +/// ```cpp +/// auto gradient = +/// LinearGradient() +/// .Angle(45) +/// .Stop(Color::Red, 0.0) +/// .Stop(Color::Green, 0.5) +/// .Stop(Color::Blue, 1.0);; +/// ``` +/// @ingroup dom +LinearGradient::LinearGradient() = default; + +/// @brief Build a gradient with two colors. +/// @param begin The color at the beginning of the gradient. +/// @param end The color at the end of the gradient. +/// @ingroup dom +LinearGradient::LinearGradient(Color begin, Color end) { + stops.push_back({begin, {}}); + stops.push_back({end, {}}); +} + +/// @brief Build a gradient with two colors and an angle. +/// @param a The angle of the gradient. +/// @param begin The color at the beginning of the gradient. +/// @param end The color at the end of the gradient. +/// @ingroup dom +LinearGradient::LinearGradient(float a, Color begin, Color end) { + angle = a; + stops.push_back({begin, {}}); + stops.push_back({end, {}}); +} + +/// @brief Set the angle of the gradient. +/// @param a The angle of the gradient. +/// @return The gradient. +/// @ingroup dom +LinearGradient& LinearGradient::Angle(float a) { + angle = a; + return *this; +} + +/// @brief Add a color stop to the gradient. +/// @param c The color of the stop. +/// @param p The position of the stop. +/// @return The gradient. +LinearGradient& LinearGradient::Stop(Color c, float p) { + stops.push_back({c, p}); + return *this; +} + +/// @brief Add a color stop to the gradient. +/// @param c The color of the stop. +/// @return The gradient. +/// @ingroup dom +/// @note The position of the stop is interpolated from nearby stops. +LinearGradient& LinearGradient::Stop(Color c) { + stops.push_back({c, {}}); + return *this; +} + +/// @brief Set the foreground color of an element with linear-gradient effect. +/// @param gradient The gradient effect to be applied on the output element. +/// @param child The input element. +/// @return The output element colored. +/// @ingroup dom +/// +/// ### Example +/// +/// ```cpp +/// color(LinearGradient{0, {Color::Red, Color::Blue}}, text("Hello")) +/// ``` +Element color(const LinearGradient& gradient, Element child) { + return std::make_shared(std::move(child), gradient, + /*background_color*/ false); +} + +/// @brief Set the background color of an element with linear-gradient effect. +/// @param gradient The gradient effect to be applied on the output element. +/// @param child The input element. +/// @return The output element colored. +/// @ingroup dom +/// +/// ### Example +/// +/// ```cpp +/// bgcolor(LinearGradient{0, {Color::Red, Color::Blue}}, text("Hello")) +/// ``` +Element bgcolor(const LinearGradient& gradient, Element child) { + return std::make_shared(std::move(child), gradient, + /*background_color*/ true); +} + +/// @brief Decorate using a linear-gradient effect on the foreground color. +/// @param gradient The gradient effect to be applied on the output element. +/// @return The Decorator applying the color. +/// @ingroup dom +/// +/// ### Example +/// +/// ```cpp +/// text("Hello") | color(LinearGradient{0, {Color::Red, Color::Blue}}) +/// ``` +Decorator color(const LinearGradient& gradient) { + return + [gradient](Element child) { return color(gradient, std::move(child)); }; +} + +/// @brief Decorate using a linear-gradient effect on the background color. +/// @param gradient The gradient effect to be applied on the output element. +/// @return The Decorator applying the color. +/// @ingroup dom +/// +/// ### Example +/// +/// ```cpp +/// text("Hello") | color(LinearGradient{0, {Color::Red, Color::Blue}}) +/// ``` +Decorator bgcolor(const LinearGradient& gradient) { + return + [gradient](Element child) { return bgcolor(gradient, std::move(child)); }; +} + +} // namespace ftxui + +// Copyright 2023 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/src/ftxui/dom/linear_gradient_test.cpp b/src/ftxui/dom/linear_gradient_test.cpp new file mode 100644 index 0000000..4f3d102 --- /dev/null +++ b/src/ftxui/dom/linear_gradient_test.cpp @@ -0,0 +1,90 @@ +#include // for Test, EXPECT_EQ, Message, TestPartResult, TestInfo (ptr only), TEST +#include // for LinearGradient::Stop, LinearGradient +#include // for allocator + +#include "ftxui/dom/elements.hpp" // for operator|, text, bgcolor, color, Element +#include "ftxui/dom/node.hpp" // for Render +#include "ftxui/screen/color.hpp" // for Color, Color::Red, Color::RedLight +#include "ftxui/screen/screen.hpp" // for Screen, Pixel + +namespace ftxui { + +TEST(ColorTest, API_default) { + LinearGradient gradient; + EXPECT_EQ(gradient.angle, 0); + EXPECT_EQ(gradient.stops.size(), 0); +} + +TEST(ColorTest, API_builder) { + auto gradient = LinearGradient() // + .Angle(45) + .Stop(Color::Red) + .Stop(Color::RedLight, 0.5) + .Stop(Color::RedLight); + EXPECT_EQ(gradient.angle, 45); + EXPECT_EQ(gradient.stops.size(), 3); + EXPECT_EQ(gradient.stops[0].color, Color::Red); + EXPECT_EQ(gradient.stops[0].position, std::nullopt); + EXPECT_EQ(gradient.stops[1].color, Color::RedLight); + EXPECT_EQ(gradient.stops[1].position, 0.5); + EXPECT_EQ(gradient.stops[2].color, Color::RedLight); + EXPECT_EQ(gradient.stops[2].position, std::nullopt); +} + +TEST(ColorTest, API_constructor_bicolor) { + auto gradient = LinearGradient(Color::Red, Color::RedLight); + EXPECT_EQ(gradient.angle, 0); + EXPECT_EQ(gradient.stops.size(), 2); + EXPECT_EQ(gradient.stops[0].color, Color::Red); + EXPECT_EQ(gradient.stops[0].position, std::nullopt); + EXPECT_EQ(gradient.stops[1].color, Color::RedLight); + EXPECT_EQ(gradient.stops[1].position, std::nullopt); +} + +TEST(ColorTest, API_constructor_bicolor_angle) { + auto gradient = LinearGradient(45, Color::Red, Color::RedLight); + EXPECT_EQ(gradient.angle, 45); + EXPECT_EQ(gradient.stops.size(), 2); + EXPECT_EQ(gradient.stops[0].color, Color::Red); + EXPECT_EQ(gradient.stops[0].position, std::nullopt); + EXPECT_EQ(gradient.stops[1].color, Color::RedLight); + EXPECT_EQ(gradient.stops[1].position, std::nullopt); +} + +TEST(ColorTest, GradientForeground) { + auto element = + text("text") | color(LinearGradient(Color::RedLight, Color::Red)); + Screen screen(5, 1); + Render(screen, element); + + Color gradient_begin = Color::Interpolate(0, Color::RedLight, Color::Red); + Color gradient_end = Color::Interpolate(1, Color::RedLight, Color::Red); + + EXPECT_EQ(screen.PixelAt(0, 0).foreground_color, gradient_begin); + EXPECT_EQ(screen.PixelAt(0, 0).background_color, Color()); + + EXPECT_EQ(screen.PixelAt(4, 0).foreground_color, gradient_end); + EXPECT_EQ(screen.PixelAt(4, 0).background_color, Color()); +} + +TEST(ColorTest, GradientBackground) { + auto element = + text("text") | bgcolor(LinearGradient(Color::RedLight, Color::Red)); + Screen screen(5, 1); + Render(screen, element); + + Color gradient_begin = Color::Interpolate(0, Color::RedLight, Color::Red); + Color gradient_end = Color::Interpolate(1, Color::RedLight, Color::Red); + + EXPECT_EQ(screen.PixelAt(0, 0).foreground_color, Color()); + EXPECT_EQ(screen.PixelAt(0, 0).background_color, gradient_begin); + + EXPECT_EQ(screen.PixelAt(4, 0).foreground_color, Color()); + EXPECT_EQ(screen.PixelAt(4, 0).background_color, gradient_end); +} + +} // namespace ftxui + +// Copyright 2023 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/src/ftxui/dom/table.cpp b/src/ftxui/dom/table.cpp index bdaf5fc..4de41ac 100644 --- a/src/ftxui/dom/table.cpp +++ b/src/ftxui/dom/table.cpp @@ -15,12 +15,12 @@ bool IsCell(int x, int y) { // NOLINTNEXTLINE static std::string charset[6][6] = { - {"┌", "┐", "└", "┘", "─", "│"}, // LIGHT - {"┏", "┓", "┗", "┛", "╍", "╏"}, // DASHED - {"┏", "┓", "┗", "┛", "━", "┃"}, // HEAVY - {"╔", "╗", "╚", "╝", "═", "║"}, // DOUBLE - {"╭", "╮", "╰", "╯", "─", "│"}, // ROUNDED - {" ", " ", " ", " ", " ", " "}, // EMPTY + {"┌", "┐", "└", "┘", "─", "│"}, // LIGHT + {"┏", "┓", "┗", "┛", "╍", "╏"}, // DASHED + {"┏", "┓", "┗", "┛", "━", "┃"}, // HEAVY + {"╔", "╗", "╚", "╝", "═", "║"}, // DOUBLE + {"╭", "╮", "╰", "╯", "─", "│"}, // ROUNDED + {" ", " ", " ", " ", " ", " "}, // EMPTY }; int Wrap(int input, int modulo) { diff --git a/src/ftxui/screen/color.cpp b/src/ftxui/screen/color.cpp index 30caf2c..f52180d 100644 --- a/src/ftxui/screen/color.cpp +++ b/src/ftxui/screen/color.cpp @@ -1,6 +1,7 @@ #include "ftxui/screen/color.hpp" -#include // for array +#include // for array +#include #include // for literals #include "ftxui/screen/color_info.hpp" // for GetColorInfo, ColorInfo @@ -220,12 +221,12 @@ Color Color::Interpolate(float t, const Color& a, const Color& b) { get_color(a, &red_a, &green_a, &blue_a); get_color(b, &red_b, &green_b, &blue_b); - return Color::RGB(static_cast(static_cast(red_a) * (1 - t) + - static_cast(red_b) * t), - static_cast(static_cast(green_a) * (1 - t) + - static_cast(green_b) * t), - static_cast(static_cast(blue_a) * (1 - t) + - static_cast(blue_b) * t)); + // Gamma correction: + // https://en.wikipedia.org/wiki/Gamma_correction + return Color::RGB( + pow(pow(red_a, 2.2f) * (1 - t) + pow(red_b, 2.2f) * t, 1 / 2.2f), + pow(pow(green_a, 2.2f) * (1 - t) + pow(green_b, 2.2f) * t, 1 / 2.2f), + pow(pow(blue_a, 2.2f) * (1 - t) + pow(blue_b, 2.2f) * t, 1 / 2.2f)); } inline namespace literals { diff --git a/src/ftxui/screen/color_test.cpp b/src/ftxui/screen/color_test.cpp index 12dbb28..4dc65df 100644 --- a/src/ftxui/screen/color_test.cpp +++ b/src/ftxui/screen/color_test.cpp @@ -57,22 +57,22 @@ TEST(ColorTest, Interpolate) { Color::RGB(1, 2, 3), // Color::RGB(244, 244, 123)) // .Print(false), - "38;2;73;74;39"); + "38;2;141;141;71"); EXPECT_EQ(Color::Interpolate(0.7f, // Color::RGB(1, 2, 3), // Color::RGB(244, 244, 123)) // .Print(false), - "38;2;171;171;87"); + "38;2;207;207;104"); EXPECT_EQ(Color::Interpolate(0.7f, // Color(Color::Red), // Color::RGB(244, 244, 123)) // .Print(false), - "38;2;209;170;86"); + "38;2;216;207;104"); EXPECT_EQ(Color::Interpolate(0.7f, // Color::RGB(244, 244, 123), // Color(Color::Plum1)) // .Print(false), - "38;2;251;195;215"); + "38;2;251;198;225"); } TEST(ColorTest, HSV) {