diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b366b..427b816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,15 @@ current (development) - Feature: `input` is now supporting multiple lines. - Feature: `input` style is now customizeable. +### Dom +- Feature: Add `hyperlink` decorator. For instance: + ```cpp + auto link = text("Click here") | hyperlink("https://github.com/FTXUI") + ``` + See the [OSC 8 page](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda). + FTXUI support proposed by @aaleino in [#662](https://github.com/ArthurSonzogni/FTXUI/issues/662). + + ### Build - Check version compatibility when using cmake find_package() - Add `FTXUI_DEV_WARNING` options to turn on warnings when building FTXUI diff --git a/CMakeLists.txt b/CMakeLists.txt index c99700f..ebed83e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,7 @@ add_library(dom src/ftxui/dom/automerge.cpp src/ftxui/dom/blink.cpp src/ftxui/dom/bold.cpp + src/ftxui/dom/hyperlink.cpp src/ftxui/dom/border.cpp src/ftxui/dom/box_helper.cpp src/ftxui/dom/box_helper.hpp diff --git a/README.md b/README.md index 4abdb48..763391d 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ An element can be decorated using the functions: - `strikethrough` - `color` - `bgcolor` + - `hyperlink` [Example](https://arthursonzogni.github.io/FTXUI/examples_2dom_2style_gallery_8cpp-example.html) diff --git a/cmake/ftxui_test.cmake b/cmake/ftxui_test.cmake index 6142d5f..4c1b15a 100644 --- a/cmake/ftxui_test.cmake +++ b/cmake/ftxui_test.cmake @@ -36,6 +36,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/hyperlink_test.cpp src/ftxui/dom/linear_gradient_test.cpp src/ftxui/dom/scroll_indicator_test.cpp src/ftxui/dom/separator_test.cpp diff --git a/examples/dom/CMakeLists.txt b/examples/dom/CMakeLists.txt index ad3285e..911bf7f 100644 --- a/examples/dom/CMakeLists.txt +++ b/examples/dom/CMakeLists.txt @@ -27,6 +27,7 @@ example(style_bold) example(style_color) example(style_dim) example(style_gallery) +example(style_hyperlink) example(style_inverted) example(style_strikethrough) example(style_underlined) diff --git a/examples/dom/style_gallery.cpp b/examples/dom/style_gallery.cpp index c958c57..419f294 100644 --- a/examples/dom/style_gallery.cpp +++ b/examples/dom/style_gallery.cpp @@ -19,7 +19,8 @@ int main() { text("blink") | blink , text(" ") , text("strikethrough") | strikethrough , text(" ") , text("color") | color(Color::Blue) , text(" ") , - text("bgcolor") | bgcolor(Color::Blue) , + text("bgcolor") | bgcolor(Color::Blue) , text(" ") , + text("hyperlink") | hyperlink("https://github.com/ArthurSonzogni/FTXUI"), }); // clang-format on auto screen = Screen::Create(Dimension::Full(), Dimension::Fit(document)); diff --git a/examples/dom/style_hyperlink.cpp b/examples/dom/style_hyperlink.cpp new file mode 100644 index 0000000..8ab092c --- /dev/null +++ b/examples/dom/style_hyperlink.cpp @@ -0,0 +1,25 @@ +#include // for text, operator|, bold, Fit, hbox, Element +#include // for Full, Screen +#include // for allocator + +#include "ftxui/dom/node.hpp" // for Render +#include "ftxui/screen/color.hpp" // for ftxui + +int main() { + using namespace ftxui; + auto document = // + hbox({ + text("This text is an "), + text("hyperlink") | hyperlink("https://www.google.com"), + text(". Do you like it?"), + }); + auto screen = Screen::Create(Dimension::Full(), Dimension::Fit(document)); + Render(screen, document); + screen.Print(); + + return 0; +} + +// Copyright 2020 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/dom/elements.hpp b/include/ftxui/dom/elements.hpp index b414c76..d343cdc 100644 --- a/include/ftxui/dom/elements.hpp +++ b/include/ftxui/dom/elements.hpp @@ -109,6 +109,8 @@ Element bgcolor(const LinearGradient&, Element); Decorator focusPosition(int x, int y); Decorator focusPositionRelative(float x, float y); Element automerge(Element child); +Decorator hyperlink(std::string link); +Element hyperlink(std::string link, Element child); // --- Layout is // Horizontal, Vertical or stacked set of elements. diff --git a/include/ftxui/screen/screen.hpp b/include/ftxui/screen/screen.hpp index d8a5329..0bb90a8 100644 --- a/include/ftxui/screen/screen.hpp +++ b/include/ftxui/screen/screen.hpp @@ -1,8 +1,9 @@ #ifndef FTXUI_SCREEN_SCREEN_HPP #define FTXUI_SCREEN_SCREEN_HPP +#include // for uint8_t #include -#include // for string, allocator, basic_string +#include // for string, basic_string, allocator #include // for vector #include "ftxui/screen/box.hpp" // for Box @@ -20,6 +21,10 @@ struct Pixel { // like: a⃦, this can potentially contains multiple codepoitns. std::string character = " "; + // The hyperlink associated with the pixel. + // 0 is the default value, meaning no hyperlink. + uint8_t hyperlink = 0; + // Colors: Color background_color = Color::Default; Color foreground_color = Color::Default; @@ -99,6 +104,11 @@ class Screen { Cursor cursor() const { return cursor_; } void SetCursor(Cursor cursor) { cursor_ = cursor; } + // Store an hyperlink in the screen. Return the id of the hyperlink. The id is + // used to identify the hyperlink when the user click on it. + uint8_t RegisterHyperlink(std::string link); + const std::string& Hyperlink(uint8_t id) const; + Box stencil; protected: @@ -106,6 +116,7 @@ class Screen { int dimy_; std::vector> pixels_; Cursor cursor_; + std::vector hyperlinks_ = {""}; }; } // namespace ftxui diff --git a/iwyu.imp b/iwyu.imp index 83d8d42..4a14640 100644 --- a/iwyu.imp +++ b/iwyu.imp @@ -14,6 +14,32 @@ { include: ["", "private", "", "public" ] }, { include: ["", "private", "", "public" ] }, { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, + { include: ["", "private", "", "public" ] }, { symbol: ["ftxui", "private", "", "public" ] }, { symbol: ["char_traits", "private", "", "public" ] }, { symbol: ["ECHO", "private", "", "public" ] }, diff --git a/src/ftxui/component/input.cpp b/src/ftxui/component/input.cpp index a854614..c19b508 100644 --- a/src/ftxui/component/input.cpp +++ b/src/ftxui/component/input.cpp @@ -1,6 +1,6 @@ -#include // for uint32_t #include // for max, min #include // for size_t +#include // for uint32_t #include // for function #include // for allocator, shared_ptr, allocator_traits<>::value_type #include // for basic_istream, stringstream diff --git a/src/ftxui/dom/hyperlink.cpp b/src/ftxui/dom/hyperlink.cpp new file mode 100644 index 0000000..6ce8a7a --- /dev/null +++ b/src/ftxui/dom/hyperlink.cpp @@ -0,0 +1,72 @@ +#include // for uint8_t +#include // for make_shared +#include // for string +#include // for move + +#include "ftxui/dom/elements.hpp" // for Element, Decorator, hyperlink +#include "ftxui/dom/node_decorator.hpp" // for NodeDecorator +#include "ftxui/screen/box.hpp" // for Box +#include "ftxui/screen/screen.hpp" // for Screen, Pixel + +namespace ftxui { + +class Hyperlink : public NodeDecorator { + public: + Hyperlink(Element child, std::string link) + : NodeDecorator(std::move(child)), link_(link) {} + + void Render(Screen& screen) override { + uint8_t hyperlink_id = screen.RegisterHyperlink(link_); + 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).hyperlink = hyperlink_id; + } + } + NodeDecorator::Render(screen); + } + + std::string link_; +}; + +/// @brief Make the rendered area clickable using a web browser. +/// The link will be opened when the user click on it. +/// This is supported only on a limited set of terminal emulator. +/// List: https://github.com/Alhadis/OSC8-Adoption/ +/// @param link The link +/// @param child The input element. +/// @return The output element with the link. +/// @ingroup dom +/// +/// ### Example +/// +/// ```cpp +/// Element document = +/// hyperlink("https://github.com/ArthurSonzogni/FTXUI", "link"); +/// ``` +Element hyperlink(std::string link, Element child) { + return std::make_shared(std::move(child), link); +} + +/// @brief Decorate using an hyperlink. +/// The link will be opened when the user click on it. +/// This is supported only on a limited set of terminal emulator. +/// List: https://github.com/Alhadis/OSC8-Adoption/ +/// @param link The link to redirect the users to. +/// @return The Decorator applying the hyperlink. +/// @ingroup dom +/// +/// ### Example +/// +/// ```cpp +/// Element document = +/// text("red") | hyperlink("https://github.com/Arthursonzogni/FTXUI"); +/// ``` +Decorator hyperlink(std::string link) { + return [link](Element child) { return hyperlink(link, 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/hyperlink_test.cpp b/src/ftxui/dom/hyperlink_test.cpp new file mode 100644 index 0000000..5579831 --- /dev/null +++ b/src/ftxui/dom/hyperlink_test.cpp @@ -0,0 +1,43 @@ +#include // for Test, EXPECT_EQ, Message, TestPartResult, TestInfo (ptr only), TEST +#include // for allocator, string + +#include "ftxui/dom/elements.hpp" // for text, hyperlink, operator|, Element, hbox +#include "ftxui/dom/node.hpp" // for Render +#include "ftxui/screen/screen.hpp" // for Screen, Pixel + +namespace ftxui { + +TEST(HyperlinkTest, Basic) { + auto element = hbox({ + text("text 1") | hyperlink("https://a.com"), + text("text 2") | hyperlink("https://b.com"), + text("text 3"), + text("text 4") | hyperlink("https://c.com"), + }); + + Screen screen(6 * 4, 1); + Render(screen, element); + + EXPECT_EQ(screen.PixelAt(0, 0).hyperlink, 1u); + EXPECT_EQ(screen.PixelAt(5, 0).hyperlink, 1u); + EXPECT_EQ(screen.PixelAt(6, 0).hyperlink, 2u); + EXPECT_EQ(screen.PixelAt(11, 0).hyperlink, 2u); + + std::string output = screen.ToString(); + EXPECT_EQ(output, + "\x1B]8;;https://a.com\x1B\\" + "text 1" + "\x1B]8;;https://b.com\x1B\\" + "text 2" + "\x1B]8;;\x1B\\" + "text 3" + "\x1B]8;;https://c.com\x1B\\" + "text 4" + "\x1B]8;;\x1B\\"); +} + +} // 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/screen/screen.cpp b/src/ftxui/screen/screen.cpp index 04cc758..0f2e377 100644 --- a/src/ftxui/screen/screen.cpp +++ b/src/ftxui/screen/screen.cpp @@ -1,7 +1,7 @@ -#include // for uint8_t +#include // for size_t #include // for operator<<, stringstream, basic_ostream, flush, cout, ostream #include // for _Rb_tree_const_iterator, map, operator!=, operator== -#include // for allocator +#include // for allocator, allocator_traits<>::value_type #include // IWYU pragma: keep #include // for pair @@ -50,11 +50,13 @@ void WindowsEmulateVT100Terminal() { #endif // NOLINTNEXTLINE(readability-function-cognitive-complexity) -void UpdatePixelStyle(std::stringstream& ss, +void UpdatePixelStyle(const Screen* screen, + std::stringstream& ss, Pixel& previous, const Pixel& next) { - if (next == previous) { - return; + // See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda + if (next.hyperlink != previous.hyperlink) { + ss << "\x1B]8;;" << screen->Hyperlink(next.hyperlink) << "\x1B\\"; } if ((!next.bold && previous.bold) || // @@ -435,20 +437,20 @@ std::string Screen::ToString() { for (int y = 0; y < dimy_; ++y) { if (y != 0) { - UpdatePixelStyle(ss, previous_pixel, final_pixel); + UpdatePixelStyle(this, ss, previous_pixel, final_pixel); ss << "\r\n"; } bool previous_fullwidth = false; for (const auto& pixel : pixels_[y]) { if (!previous_fullwidth) { - UpdatePixelStyle(ss, previous_pixel, pixel); + UpdatePixelStyle(this, ss, previous_pixel, pixel); ss << pixel.character; } previous_fullwidth = (string_width(pixel.character) == 2); } } - UpdatePixelStyle(ss, previous_pixel, final_pixel); + UpdatePixelStyle(this, ss, previous_pixel, final_pixel); return ss.str(); } @@ -517,6 +519,10 @@ void Screen::Clear() { } cursor_.x = dimx_ - 1; cursor_.y = dimy_ - 1; + + hyperlinks_ = { + "", + }; } // clang-format off @@ -545,9 +551,28 @@ void Screen::ApplyShader() { } } } - // clang-format on +uint8_t Screen::RegisterHyperlink(std::string link) { + for (size_t i = 0; i < hyperlinks_.size(); ++i) { + if (hyperlinks_[i] == link) { + return i; + } + } + if (hyperlinks_.size() == 255) { + return 0; + } + hyperlinks_.push_back(link); + return hyperlinks_.size() - 1; +} + +const std::string& Screen::Hyperlink(uint8_t id) const { + if (id >= hyperlinks_.size()) { + return hyperlinks_[0]; + } + return hyperlinks_[id]; +} + } // namespace ftxui // Copyright 2020 Arthur Sonzogni. All rights reserved.