From c24a27429248be927486555bcd69d172c7751912 Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <sonzogniarthur@gmail.com>
Date: Tue, 26 Sep 2023 23:08:42 +0200
Subject: [PATCH] Feature: `hscroll_indicator` (#753)

This is the symetrical of `vscroll_indicator`.

Requested by @ibrahimnasson.

Fixed:https://github.com/ArthurSonzogni/FTXUI/issues/752
---
 CHANGELOG.md                                  |  5 ++
 examples/component/CMakeLists.txt             |  1 +
 examples/component/button.cpp                 | 21 +++---
 .../component/menu_in_frame_horizontal.cpp    | 30 ++++++++
 include/ftxui/dom/elements.hpp                |  1 +
 src/ftxui/component/screen_interactive.cpp    |  2 +-
 src/ftxui/dom/color.cpp                       |  2 +-
 src/ftxui/dom/dbox.cpp                        |  2 +-
 src/ftxui/dom/frame.cpp                       |  2 -
 src/ftxui/dom/gauge.cpp                       |  2 +-
 src/ftxui/dom/scroll_indicator.cpp            | 61 +++++++++++++++-
 src/ftxui/dom/scroll_indicator_test.cpp       | 71 ++++++++++++++++++-
 src/ftxui/dom/size.cpp                        |  2 +-
 src/ftxui/dom/table.cpp                       |  1 -
 src/ftxui/screen/string.cpp                   |  2 +-
 src/ftxui/screen/string_test.cpp              |  2 +-
 16 files changed, 185 insertions(+), 22 deletions(-)
 create mode 100644 examples/component/menu_in_frame_horizontal.cpp

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ec06a130..d931aa93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,11 @@ current (development)
 - Feature: Add support for `Input`'s insert mode. Add `InputOption::insert`
   option. Added by @mingsheng13.
 
+### Dom
+- Feature: Add `hscroll_indicator`. It display an horizontal indicator
+  reflecting the current scroll position. Proposed by @ibrahimnasson in
+  [issue 752](https://github.com/ArthurSonzogni/FTXUI/issues/752)
+
 ### Build
 - Support for cmake's "unity/jumbo" builds. Fixed by @ClausKlein.
 
diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt
index 4ac82eb6..661fee06 100644
--- a/examples/component/CMakeLists.txt
+++ b/examples/component/CMakeLists.txt
@@ -25,6 +25,7 @@ example(menu2)
 example(menu_entries)
 example(menu_entries_animated)
 example(menu_in_frame)
+example(menu_in_frame_horizontal)
 example(menu_multiple)
 example(menu_style)
 example(menu_underline_animated_gallery)
diff --git a/examples/component/button.cpp b/examples/component/button.cpp
index 7629292e..dda9dd3e 100644
--- a/examples/component/button.cpp
+++ b/examples/component/button.cpp
@@ -33,19 +33,21 @@ ButtonOption ButtonStyle() {
 int main() {
   int value = 50;
 
-
   // The tree of components. This defines how to navigate using the keyboard.
-  auto buttons =
-    Container::Vertical({
+  auto buttons = Container::Vertical({
       Container::Horizontal({
-        Button("-1", [&] { value--; }, ButtonStyle()),
-        Button("+1", [&] { value++; }, ButtonStyle()),
+          Button(
+              "-1", [&] { value--; }, ButtonStyle()),
+          Button(
+              "+1", [&] { value++; }, ButtonStyle()),
       }) | flex,
       Container::Horizontal({
-        Button("-10", [&] { value -= 10; }, ButtonStyle()),
-        Button("+10", [&] { value += 10; }, ButtonStyle()),
+          Button(
+              "-10", [&] { value -= 10; }, ButtonStyle()),
+          Button(
+              "+10", [&] { value += 10; }, ButtonStyle()),
       }) | flex,
-    });
+  });
 
   // Modify the way to render them on screen:
   auto component = Renderer(buttons, [&] {
@@ -53,7 +55,8 @@ int main() {
                text("value = " + std::to_string(value)),
                separator(),
                buttons->Render() | flex,
-           }) | flex | border;
+           }) |
+           flex | border;
   });
 
   auto screen = ScreenInteractive::Fullscreen();
diff --git a/examples/component/menu_in_frame_horizontal.cpp b/examples/component/menu_in_frame_horizontal.cpp
new file mode 100644
index 00000000..1ba4a749
--- /dev/null
+++ b/examples/component/menu_in_frame_horizontal.cpp
@@ -0,0 +1,30 @@
+// 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.
+#include <memory>  // for shared_ptr, __shared_ptr_access
+#include <string>  // for string, basic_string, operator+, to_string
+#include <vector>  // for vector
+
+#include "ftxui/component/captured_mouse.hpp"      // for ftxui
+#include "ftxui/component/component.hpp"           // for Radiobox, Renderer
+#include "ftxui/component/component_base.hpp"      // for ComponentBase
+#include "ftxui/component/screen_interactive.hpp"  // for ScreenInteractive
+#include "ftxui/dom/elements.hpp"  // for operator|, Element, size, border, frame, HEIGHT, LESS_THAN
+
+using namespace ftxui;
+
+int main() {
+  std::vector<std::string> entries;
+  int selected = 0;
+
+  for (int i = 0; i < 100; ++i)
+    entries.push_back(std::to_string(i));
+  auto radiobox = Menu(&entries, &selected, MenuOption::Horizontal());
+  auto renderer = Renderer(
+      radiobox, [&] { return radiobox->Render() | hscroll_indicator | frame; });
+
+  auto screen = ScreenInteractive::FitComponent();
+  screen.Loop(renderer);
+
+  return 0;
+}
diff --git a/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp
index ffd93601..721674af 100644
--- a/include/ftxui/dom/elements.hpp
+++ b/include/ftxui/dom/elements.hpp
@@ -170,6 +170,7 @@ Element focusCursorUnderlineBlinking(Element);
 
 // --- Misc ---
 Element vscroll_indicator(Element);
+Element hscroll_indicator(Element);
 Decorator reflect(Box& box);
 // Before drawing the |element| clear the pixel below. This is useful in
 // combinaison with dbox.
diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp
index 24869a0f..ca908609 100644
--- a/src/ftxui/component/screen_interactive.cpp
+++ b/src/ftxui/component/screen_interactive.cpp
@@ -402,7 +402,7 @@ void ScreenInteractive::TrackMouse(bool enable) {
   track_mouse_ = enable;
 }
 
-/// @brief Add a task to the main loop. 
+/// @brief Add a task to the main loop.
 /// It will be executed later, after every other scheduled tasks.
 /// @ingroup component
 void ScreenInteractive::Post(Task task) {
diff --git a/src/ftxui/dom/color.cpp b/src/ftxui/dom/color.cpp
index 859b8a1a..238ccca6 100644
--- a/src/ftxui/dom/color.cpp
+++ b/src/ftxui/dom/color.cpp
@@ -12,7 +12,7 @@
 
 namespace ftxui {
 
-  namespace {
+namespace {
 class BgColor : public NodeDecorator {
  public:
   BgColor(Element child, Color color)
diff --git a/src/ftxui/dom/dbox.cpp b/src/ftxui/dom/dbox.cpp
index af87d207..e26d3dde 100644
--- a/src/ftxui/dom/dbox.cpp
+++ b/src/ftxui/dom/dbox.cpp
@@ -13,7 +13,7 @@
 
 namespace ftxui {
 
-  namespace {
+namespace {
 class DBox : public Node {
  public:
   explicit DBox(Elements children) : Node(std::move(children)) {}
diff --git a/src/ftxui/dom/frame.cpp b/src/ftxui/dom/frame.cpp
index fe974403..99f19c87 100644
--- a/src/ftxui/dom/frame.cpp
+++ b/src/ftxui/dom/frame.cpp
@@ -37,7 +37,6 @@ class Select : public Node {
   }
 };
 
-
 class Focus : public Select {
  public:
   using Select::Select;
@@ -143,7 +142,6 @@ class FocusCursor : public Focus {
   Screen::Cursor::Shape shape_;
 };
 
-
 }  // namespace
 
 /// @brief Set the `child` to be the one selected among its siblings.
diff --git a/src/ftxui/dom/gauge.cpp b/src/ftxui/dom/gauge.cpp
index d6436aaf..19926a2d 100644
--- a/src/ftxui/dom/gauge.cpp
+++ b/src/ftxui/dom/gauge.cpp
@@ -159,7 +159,7 @@ class Gauge : public Node {
   Direction direction_;
 };
 
-}  // namespace ftxui
+}  // namespace
 
 /// @brief Draw a high definition progress bar progressing in specified
 /// direction.
diff --git a/src/ftxui/dom/scroll_indicator.cpp b/src/ftxui/dom/scroll_indicator.cpp
index a744ddc5..05480bb0 100644
--- a/src/ftxui/dom/scroll_indicator.cpp
+++ b/src/ftxui/dom/scroll_indicator.cpp
@@ -7,7 +7,7 @@
 #include <utility>    // for move
 #include <vector>     // for __alloc_traits<>::value_type
 
-#include "ftxui/dom/elements.hpp"        // for Element, vscroll_indicator
+#include "ftxui/dom/elements.hpp"  // for Element, vscroll_indicator, hscroll_indicator
 #include "ftxui/dom/node.hpp"            // for Node, Elements
 #include "ftxui/dom/node_decorator.hpp"  // for NodeDecorator
 #include "ftxui/dom/requirement.hpp"     // for Requirement
@@ -16,7 +16,7 @@
 
 namespace ftxui {
 
-/// @brief Add a filter that will invert the foreground and the background
+/// @brief Display a vertical scrollbar to the right.
 /// colors.
 /// @ingroup dom
 Element vscroll_indicator(Element child) {
@@ -72,4 +72,61 @@ Element vscroll_indicator(Element child) {
   return std::make_shared<Impl>(std::move(child));
 }
 
+/// @brief Display an horizontal scrollbar to the bottom.
+/// colors.
+/// @ingroup dom
+Element hscroll_indicator(Element child) {
+  class Impl : public NodeDecorator {
+    using NodeDecorator::NodeDecorator;
+
+    void ComputeRequirement() override {
+      NodeDecorator::ComputeRequirement();
+      requirement_ = children_[0]->requirement();
+      requirement_.min_y++;
+    }
+
+    void SetBox(Box box) override {
+      box_ = box;
+      box.y_max--;
+      children_[0]->SetBox(box);
+    }
+
+    void Render(Screen& screen) final {
+      NodeDecorator::Render(screen);
+
+      const Box& stencil = screen.stencil;
+
+      const int size_inner = box_.x_max - box_.x_min;
+      if (size_inner <= 0) {
+        return;
+      }
+      const int size_outter = stencil.x_max - stencil.x_min + 1;
+      if (size_outter >= size_inner) {
+        return;
+      }
+
+      int size = 2 * size_outter * size_outter / size_inner;
+      size = std::max(size, 1);
+
+      const int start_x =
+          2 * stencil.x_min +  //
+          2 * (stencil.x_min - box_.x_min) * size_outter / size_inner;
+
+      const int y = stencil.y_max;
+      for (int x = stencil.x_min; x <= stencil.x_max; ++x) {
+        const int x_left = 2 * x + 0;
+        const int x_right = 2 * x + 1;
+        const bool left = (start_x <= x_left) && (x_left <= start_x + size);
+        const bool right = (start_x <= x_right) && (x_right <= start_x + size);
+
+        const char* c =
+            left ? (right ? "─" : "╴") : (right ? "╶" : " ");  // NOLINT
+        screen.PixelAt(x, y) = Pixel();
+        screen.PixelAt(x, y).character = c;
+      }
+    }
+  };
+  return std::make_shared<Impl>(std::move(child));
+}
+
 }  // namespace ftxui
diff --git a/src/ftxui/dom/scroll_indicator_test.cpp b/src/ftxui/dom/scroll_indicator_test.cpp
index 3f8e1cfd..3b104b19 100644
--- a/src/ftxui/dom/scroll_indicator_test.cpp
+++ b/src/ftxui/dom/scroll_indicator_test.cpp
@@ -26,6 +26,18 @@ Element MakeVerticalList(int focused_index, int n) {
   return vbox(std::move(list)) | vscroll_indicator | frame | border;
 }
 
+Element MakeHorizontalList(int focused_index, int n) {
+  Elements list;
+  for (int i = 0; i < n; ++i) {
+    auto element = text(std::to_string(i));
+    if (i == focused_index) {
+      element |= focus;
+    }
+    list.push_back(element);
+  }
+  return hbox(std::move(list)) | hscroll_indicator | frame | border;
+}
+
 std::string PrintVerticalList(int focused_index, int n) {
   auto element = MakeVerticalList(focused_index, n);
   Screen screen(6, 6);
@@ -33,9 +45,16 @@ std::string PrintVerticalList(int focused_index, int n) {
   return screen.ToString();
 }
 
+std::string PrintHorizontalList(int focused_index, int n) {
+  auto element = MakeHorizontalList(focused_index, n);
+  Screen screen(6, 4);
+  Render(screen, element);
+  return screen.ToString();
+}
+
 }  // namespace
 
-TEST(ScrollIndicator, Basic) {
+TEST(ScrollIndicator, BasicVertical) {
   EXPECT_EQ(PrintVerticalList(0, 10),
             "╭────╮\r\n"
             "│0  ┃│\r\n"
@@ -108,6 +127,56 @@ TEST(ScrollIndicator, Basic) {
             "╰────╯");
 }
 
+TEST(ScrollIndicator, BasicHorizontal) {
+  EXPECT_EQ(PrintHorizontalList(0, 10),
+            "╭────╮\r\n"
+            "│0123│\r\n"
+            "│──  │\r\n"
+            "╰────╯");
+
+  EXPECT_EQ(PrintHorizontalList(1, 10),
+            "╭────╮\r\n"
+            "│0123│\r\n"
+            "│──  │\r\n"
+            "╰────╯");
+
+  EXPECT_EQ(PrintHorizontalList(2, 10),
+            "╭────╮\r\n"
+            "│1234│\r\n"
+            "│──  │\r\n"
+            "╰────╯");
+  EXPECT_EQ(PrintHorizontalList(3, 10),
+            "╭────╮\r\n"
+            "│2345│\r\n"
+            "│╶─╴ │\r\n"
+            "╰────╯");
+  EXPECT_EQ(PrintHorizontalList(4, 10),
+            "╭────╮\r\n"
+            "│3456│\r\n"
+            "│ ── │\r\n"
+            "╰────╯");
+  EXPECT_EQ(PrintHorizontalList(5, 10),
+            "╭────╮\r\n"
+            "│4567│\r\n"
+            "│ ╶─╴│\r\n"
+            "╰────╯");
+  EXPECT_EQ(PrintHorizontalList(6, 10),
+            "╭────╮\r\n"
+            "│5678│\r\n"
+            "│  ──│\r\n"
+            "╰────╯");
+  EXPECT_EQ(PrintHorizontalList(7, 10),
+            "╭────╮\r\n"
+            "│6789│\r\n"
+            "│  ──│\r\n"
+            "╰────╯");
+  EXPECT_EQ(PrintHorizontalList(8, 10),
+            "╭────╮\r\n"
+            "│6789│\r\n"
+            "│  ──│\r\n"
+            "╰────╯");
+}
+
 namespace {
 
 Element MakeHorizontalFlexboxList(int n) {
diff --git a/src/ftxui/dom/size.cpp b/src/ftxui/dom/size.cpp
index 91fa702b..06351e2b 100644
--- a/src/ftxui/dom/size.cpp
+++ b/src/ftxui/dom/size.cpp
@@ -79,7 +79,7 @@ class Size : public Node {
   Constraint constraint_;
   int value_;
 };
-} // namespace
+}  // namespace
 
 /// @brief Apply a constraint on the size of an element.
 /// @param direction Whether the WIDTH of the HEIGHT of the element must be
diff --git a/src/ftxui/dom/table.cpp b/src/ftxui/dom/table.cpp
index 4927ea93..76b91cb6 100644
--- a/src/ftxui/dom/table.cpp
+++ b/src/ftxui/dom/table.cpp
@@ -47,7 +47,6 @@ Table::Table() {
   Initialize({});
 }
 
-
 /// @brief Create a table from a vector of vector of string.
 /// @param input The input data.
 /// @ingroup dom
diff --git a/src/ftxui/screen/string.cpp b/src/ftxui/screen/string.cpp
index 0b5630c6..48012f0f 100644
--- a/src/ftxui/screen/string.cpp
+++ b/src/ftxui/screen/string.cpp
@@ -1,7 +1,7 @@
 // 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.
-// 
+//
 // Content of this file was created thanks to:
 // -
 // https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/WordBreakProperty.txt
diff --git a/src/ftxui/screen/string_test.cpp b/src/ftxui/screen/string_test.cpp
index 27b3e65b..ee8a5005 100644
--- a/src/ftxui/screen/string_test.cpp
+++ b/src/ftxui/screen/string_test.cpp
@@ -164,4 +164,4 @@ TEST(StringTest, to_wstring) {
   EXPECT_EQ(to_wstring(std::string("🎅🎄")), L"🎅🎄");
 }
 
-}
+}  // namespace ftxui
-- 
GitLab