From 6fafa2dfed9e1d5d4e56660206fec7fb8e1af7dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cl=C3=A9ment=20Roblot?= <clement.roblot@martobre.fr>
Date: Fri, 27 Dec 2024 15:45:13 +0700
Subject: [PATCH] Feature: Selection

Add support for selection content in the dom.
---
 CHANGELOG.md                                  |   9 +
 CMakeLists.txt                                |   5 +-
 cmake/ftxui_test.cmake                        |   1 +
 examples/component/CMakeLists.txt             |   1 +
 examples/component/gallery.cpp                |  23 +-
 examples/component/homescreen.cpp             |   2 +-
 examples/component/selection.cpp              |  87 +++++++
 .../ftxui/component/screen_interactive.hpp    |  23 ++
 include/ftxui/dom/elements.hpp                |   5 +
 include/ftxui/dom/node.hpp                    |  13 +-
 include/ftxui/dom/selection.hpp               |  50 ++++
 include/ftxui/screen/screen.hpp               |  17 +-
 src/ftxui/component/container.cpp             |   3 +-
 src/ftxui/component/screen_interactive.cpp    | 101 +++++++-
 src/ftxui/dom/flex.cpp                        |   1 +
 src/ftxui/dom/hbox.cpp                        |  13 +
 src/ftxui/dom/node.cpp                        |  74 +++++-
 src/ftxui/dom/paragraph.cpp                   |  48 ++--
 src/ftxui/dom/selection.cpp                   | 168 +++++++++++++
 src/ftxui/dom/selection_style.cpp             |  89 +++++++
 src/ftxui/dom/selection_test.cpp              | 224 ++++++++++++++++++
 src/ftxui/dom/text.cpp                        |  47 +++-
 src/ftxui/dom/vbox.cpp                        |  14 ++
 src/ftxui/screen/screen.cpp                   |  12 +
 tools/license_headers.cpp                     |   2 +-
 25 files changed, 1001 insertions(+), 31 deletions(-)
 create mode 100644 examples/component/selection.cpp
 create mode 100644 include/ftxui/dom/selection.hpp
 create mode 100644 src/ftxui/dom/selection.cpp
 create mode 100644 src/ftxui/dom/selection_style.cpp
 create mode 100644 src/ftxui/dom/selection_test.cpp

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfbcfb15..fbe57ae2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,9 @@ current (development)
 - Feature: Add support for `Input`'s insert mode. Add `InputOption::insert`
   option. Added by @mingsheng13.
 - Feature: Add `DropdownOption` to configure the dropdown. See #826.
+- Feature: Add support for Selection. Thanks @clement-roblot. See #926.
+  - See `ScreenInteractive::GetSelection()`.
+  - See `ScreenInteractive::SelectionChange(...)` listener.
 - Bugfix/Breaking change: `Mouse transition`:
   - Detect when the mouse move, as opposed to being pressed.
     The Mouse::Moved motion was added.
@@ -49,6 +52,12 @@ current (development)
 - Feature: Add `extend_beyond_screen` option to `Dimension::Fit(..)`, allowing
   the element to be larger than the screen. Proposed by @LordWhiro. See #572 and
   #949.
+- Feature: Add support for Selection. Thanks @clement-roblot. See #926.
+  - See `selectionColor` decorator.
+  - See `selectionBackgroundColor` decorator.
+  - See `selectionForegroundColor` decorator.
+  - See `selectionStyle(style)` decorator.
+  - See `selectionStyleReset` decorator.
 
 ### Screen
 - Feature: Add `Box::IsEmpty()`.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bf4a9dac..2c121b1a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -56,11 +56,12 @@ add_library(dom
   include/ftxui/dom/flexbox_config.hpp
   include/ftxui/dom/node.hpp
   include/ftxui/dom/requirement.hpp
+  include/ftxui/dom/selection.hpp
   include/ftxui/dom/take_any_args.hpp
   src/ftxui/dom/automerge.cpp
+  src/ftxui/dom/selection_style.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
@@ -81,6 +82,7 @@ add_library(dom
   src/ftxui/dom/graph.cpp
   src/ftxui/dom/gridbox.cpp
   src/ftxui/dom/hbox.cpp
+  src/ftxui/dom/hyperlink.cpp
   src/ftxui/dom/inverted.cpp
   src/ftxui/dom/linear_gradient.cpp
   src/ftxui/dom/node.cpp
@@ -88,6 +90,7 @@ add_library(dom
   src/ftxui/dom/paragraph.cpp
   src/ftxui/dom/reflect.cpp
   src/ftxui/dom/scroll_indicator.cpp
+  src/ftxui/dom/selection.cpp
   src/ftxui/dom/separator.cpp
   src/ftxui/dom/size.cpp
   src/ftxui/dom/spinner.cpp
diff --git a/cmake/ftxui_test.cmake b/cmake/ftxui_test.cmake
index c75ea431..56b21bd9 100644
--- a/cmake/ftxui_test.cmake
+++ b/cmake/ftxui_test.cmake
@@ -40,6 +40,7 @@ add_executable(ftxui-tests
   src/ftxui/dom/hyperlink_test.cpp
   src/ftxui/dom/linear_gradient_test.cpp
   src/ftxui/dom/scroll_indicator_test.cpp
+  src/ftxui/dom/selection_test.cpp
   src/ftxui/dom/separator_test.cpp
   src/ftxui/dom/spinner_test.cpp
   src/ftxui/dom/table_test.cpp
diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt
index 43392152..62318b03 100644
--- a/examples/component/CMakeLists.txt
+++ b/examples/component/CMakeLists.txt
@@ -38,6 +38,7 @@ example(radiobox)
 example(radiobox_in_frame)
 example(renderer)
 example(resizable_split)
+example(selection)
 example(scrollbar)
 example(slider)
 example(slider_direction)
diff --git a/examples/component/gallery.cpp b/examples/component/gallery.cpp
index 6add4f8b..9c51120b 100644
--- a/examples/component/gallery.cpp
+++ b/examples/component/gallery.cpp
@@ -97,7 +97,25 @@ int main() {
   });
   sliders = Wrap("Slider", sliders);
 
-  // -- Layout -----------------------------------------------------------------
+  // A large text:
+  auto lorel_ipsum = Renderer([] {
+    return vbox({
+        text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. "),
+        text("Sed do eiusmod tempor incididunt ut labore et dolore magna "
+             "aliqua. "),
+        text("Ut enim ad minim veniam, quis nostrud exercitation ullamco "
+             "laboris nisi ut aliquip ex ea commodo consequat. "),
+        text("Duis aute irure dolor in reprehenderit in voluptate velit esse "
+             "cillum dolore eu fugiat nulla pariatur. "),
+        text("Excepteur sint occaecat cupidatat non proident, sunt in culpa "
+             "qui officia deserunt mollit anim id est laborum. "),
+
+    });
+  });
+  lorel_ipsum = Wrap("Lorel Ipsum", lorel_ipsum);
+
+  // -- Layout
+  // -----------------------------------------------------------------
   auto layout = Container::Vertical({
       menu,
       toggle,
@@ -106,6 +124,7 @@ int main() {
       input,
       sliders,
       button,
+      lorel_ipsum,
   });
 
   auto component = Renderer(layout, [&] {
@@ -123,6 +142,8 @@ int main() {
                sliders->Render(),
                separator(),
                button->Render(),
+               separator(),
+               lorel_ipsum->Render(),
            }) |
            xflex | size(WIDTH, GREATER_THAN, 40) | border;
   });
diff --git a/examples/component/homescreen.cpp b/examples/component/homescreen.cpp
index d650ba2e..7a5e710c 100644
--- a/examples/component/homescreen.cpp
+++ b/examples/component/homescreen.cpp
@@ -424,7 +424,7 @@ int main() {
   auto paragraph_renderer_left = Renderer([&] {
     std::string str =
         "Lorem Ipsum is simply dummy text of the printing and typesetting "
-        "industry. Lorem Ipsum has been the industry's standard dummy text "
+        "industry.\nLorem Ipsum has been the industry's standard dummy text "
         "ever since the 1500s, when an unknown printer took a galley of type "
         "and scrambled it to make a type specimen book.";
     return vbox({
diff --git a/examples/component/selection.cpp b/examples/component/selection.cpp
new file mode 100644
index 00000000..93e96ea1
--- /dev/null
+++ b/examples/component/selection.cpp
@@ -0,0 +1,87 @@
+// 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 <string>  // for char_traits, operator+, string, basic_string
+
+#include "ftxui/component/component.hpp"       // for Input, Renderer, Vertical
+#include "ftxui/component/component_base.hpp"  // for ComponentBase
+#include "ftxui/component/component_options.hpp"  // for InputOption
+#include "ftxui/component/screen_interactive.hpp"  // for Component, ScreenInteractive
+#include "ftxui/dom/elements.hpp"  // for text, hbox, separator, Element, operator|, vbox, border
+#include "ftxui/util/ref.hpp"  // for Ref
+
+using namespace ftxui;
+
+Element LoremIpsum() {
+  return vbox({
+      text("FTXUI: A powerful library for building user interfaces."),
+      text("Enjoy a rich set of components and a declarative style."),
+      text("Create beautiful and responsive UIs with minimal effort."),
+      text("Join the community and experience the power of FTXUI."),
+  });
+}
+
+int main() {
+  auto screen = ScreenInteractive::TerminalOutput();
+
+  auto quit =
+      Button("Quit", screen.ExitLoopClosure(), ButtonOption::Animated());
+
+  int selection_change_counter = 0;
+  std::string selection_content = "";
+  screen.SelectionChange([&] {
+    selection_change_counter++;
+    selection_content = screen.GetSelection();
+  });
+
+  // The components:
+  auto renderer = Renderer(quit, [&] {
+    return vbox({
+        text("Select changed: " + std::to_string(selection_change_counter) +
+             " times"),
+        text("Currently selected: "),
+        paragraph(selection_content) | vscroll_indicator | frame | border |
+            size(HEIGHT, EQUAL, 10),
+        window(text("Horizontal split"), hbox({
+                                             LoremIpsum(),
+                                             separator(),
+                                             LoremIpsum(),
+                                             separator(),
+                                             LoremIpsum(),
+                                         })),
+        window(text("Vertical split"), vbox({
+                                           LoremIpsum(),
+                                           separator(),
+                                           LoremIpsum(),
+                                           separator(),
+                                           LoremIpsum(),
+                                       })),
+        window(text("Grid split with different style"),
+               vbox({
+                   hbox({
+                       LoremIpsum(),
+                       separator(),
+                       LoremIpsum()                                   //
+                           | selectionBackgroundColor(Color::Yellow)  //
+                           | selectionColor(Color::Black)             //
+                           | selectionStyleReset,
+                       separator(),
+                       LoremIpsum() | selectionColor(Color::Blue),
+                   }),
+                   separator(),
+                   hbox({
+                       LoremIpsum() | selectionColor(Color::Red),
+                       separator(),
+                       LoremIpsum() | selectionStyle([](Pixel& pixel) {
+                         pixel.underlined_double = true;
+                       }),
+                       separator(),
+                       LoremIpsum(),
+                   }),
+               })),
+        quit->Render(),
+    });
+  });
+
+  screen.Loop(renderer);
+}
diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp
index 6c799136..a2909fca 100644
--- a/include/ftxui/component/screen_interactive.hpp
+++ b/include/ftxui/component/screen_interactive.hpp
@@ -16,6 +16,7 @@
 #include "ftxui/component/captured_mouse.hpp"  // for CapturedMouse
 #include "ftxui/component/event.hpp"           // for Event
 #include "ftxui/component/task.hpp"            // for Task, Closure
+#include "ftxui/dom/selection.hpp"             // for SelectionOption
 #include "ftxui/screen/screen.hpp"             // for Screen
 
 namespace ftxui {
@@ -68,6 +69,10 @@ class ScreenInteractive : public Screen {
   void ForceHandleCtrlC(bool force);
   void ForceHandleCtrlZ(bool force);
 
+  // Selection API.
+  std::string GetSelection();
+  void SelectionChange(std::function<void()> callback);
+
  private:
   void ExitNow();
 
@@ -82,6 +87,8 @@ class ScreenInteractive : public Screen {
   void RunOnceBlocking(Component component);
 
   void HandleTask(Component component, Task& task);
+  bool HandleSelection(bool handled, Event event);
+  void RefreshSelection();
   void Draw(Component component);
   void ResetCursorPosition();
 
@@ -129,6 +136,22 @@ class ScreenInteractive : public Screen {
   // The style of the cursor to restore on exit.
   int cursor_reset_shape_ = 1;
 
+  // Selection API:
+  CapturedMouse selection_pending_;
+  struct SelectionData {
+    int start_x = -1;
+    int start_y = -1;
+    int end_x = -2;
+    int end_y = -2;
+    bool empty = true;
+    bool operator==(const SelectionData& other) const;
+    bool operator!=(const SelectionData& other) const;
+  };
+  SelectionData selection_data_;
+  SelectionData selection_data_previous_;
+  std::unique_ptr<Selection> selection_;
+  std::function<void()> selection_on_change_;
+
   friend class Loop;
 
  public:
diff --git a/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp
index a2dd0442..fa16080e 100644
--- a/include/ftxui/dom/elements.hpp
+++ b/include/ftxui/dom/elements.hpp
@@ -113,6 +113,11 @@ Decorator focusPositionRelative(float x, float y);
 Element automerge(Element child);
 Decorator hyperlink(std::string link);
 Element hyperlink(std::string link, Element child);
+Element selectionStyleReset(Element);
+Decorator selectionColor(Color foreground);
+Decorator selectionBackgroundColor(Color foreground);
+Decorator selectionForegroundColor(Color foreground);
+Decorator selectionStyle(std::function<void(Pixel&)> style);
 
 // --- Layout is
 // Horizontal, Vertical or stacked set of elements.
diff --git a/include/ftxui/dom/node.hpp b/include/ftxui/dom/node.hpp
index f43157a9..87edce77 100644
--- a/include/ftxui/dom/node.hpp
+++ b/include/ftxui/dom/node.hpp
@@ -8,6 +8,7 @@
 #include <vector>  // for vector
 
 #include "ftxui/dom/requirement.hpp"  // for Requirement
+#include "ftxui/dom/selection.hpp"    // for Selection
 #include "ftxui/screen/box.hpp"       // for Box
 #include "ftxui/screen/screen.hpp"
 
@@ -40,9 +41,15 @@ class Node {
   //         Propagated from Parents to Children.
   virtual void SetBox(Box box);
 
-  // Step 3: Draw this element.
+  // Step 3: (optional) Selection
+  //         Propagated from Parents to Children.
+  virtual void Select(Selection& selection);
+
+  // Step 4: Draw this element.
   virtual void Render(Screen& screen);
 
+  virtual std::string GetSelectedContent(Selection& selection);
+
   // Layout may not resolve within a single iteration for some elements. This
   // allows them to request additionnal iterations. This signal must be
   // forwarded to children at least once.
@@ -60,6 +67,10 @@ class Node {
 
 void Render(Screen& screen, const Element& element);
 void Render(Screen& screen, Node* node);
+void Render(Screen& screen, Node* node, Selection& selection);
+std::string GetNodeSelectedContent(Screen& screen,
+                                   Node* node,
+                                   Selection& selection);
 
 }  // namespace ftxui
 
diff --git a/include/ftxui/dom/selection.hpp b/include/ftxui/dom/selection.hpp
new file mode 100644
index 00000000..3ec0e482
--- /dev/null
+++ b/include/ftxui/dom/selection.hpp
@@ -0,0 +1,50 @@
+// Copyright 2024 Arthur Sonzogni. All rights reserved.
+// Use of this source code is governed by the MIT license that can be found in
+// the LICENSE file.
+
+#ifndef FTXUI_DOM_SELECTION_HPP
+#define FTXUI_DOM_SELECTION_HPP
+
+#include <functional>
+
+#include <sstream>
+#include "ftxui/screen/box.hpp"    // for Box
+#include "ftxui/screen/pixel.hpp"  // for Pixel
+
+namespace ftxui {
+
+/// @brief Represent a selection in the terminal.
+class Selection {
+ public:
+  Selection();  // Empty selection.
+  Selection(int start_x, int start_y, int end_x, int end_y);
+
+  const Box& GetBox() const;
+
+  Selection SaturateHorizontal(Box box);
+  Selection SaturateVertical(Box box);
+  bool IsEmpty() const { return empty_; }
+
+  void AddPart(const std::string& part, int y, int left, int right);
+  std::string GetParts() { return parts_.str(); }
+
+ private:
+  Selection(int start_x, int start_y, int end_x, int end_y, Selection* parent);
+
+  Selection* const parent_ = this;
+  const bool empty_ = true;
+  const int start_x_ = 0;
+  const int start_y_ = 0;
+  const int end_x_ = 0;
+  const int end_y_ = 0;
+  const Box box_ = {};
+  std::stringstream parts_;
+
+  // The position of the last inserted part.
+  int x_ = 0;
+  int y_ = 0;
+};
+
+}  // namespace ftxui
+
+#endif /* end of include guard: FTXUI_DOM_SELECTION_HPP */
diff --git a/include/ftxui/screen/screen.hpp b/include/ftxui/screen/screen.hpp
index 51b83a03..aa2f4f8c 100644
--- a/include/ftxui/screen/screen.hpp
+++ b/include/ftxui/screen/screen.hpp
@@ -4,12 +4,14 @@
 #ifndef FTXUI_SCREEN_SCREEN_HPP
 #define FTXUI_SCREEN_SCREEN_HPP
 
-#include <cstdint>  // for uint8_t
-#include <string>   // for string, basic_string, allocator
-#include <vector>   // for vector
+#include <cstdint>     // for uint8_t
+#include <functional>  // for function
+#include <string>      // for string, basic_string, allocator
+#include <vector>      // for vector
 
 #include "ftxui/screen/image.hpp"     // for Pixel, Image
 #include "ftxui/screen/terminal.hpp"  // for Dimensions
+#include "ftxui/util/autoreset.hpp"   // for AutoReset
 
 namespace ftxui {
 
@@ -67,9 +69,18 @@ class Screen : public Image {
   uint8_t RegisterHyperlink(const std::string& link);
   const std::string& Hyperlink(uint8_t id) const;
 
+  using SelectionStyle = std::function<void(Pixel&)>;
+  const SelectionStyle& GetSelectionStyle() const;
+  void SetSelectionStyle(SelectionStyle decorator);
+
  protected:
   Cursor cursor_;
   std::vector<std::string> hyperlinks_ = {""};
+
+  // The current selection style. This is overridden by various dom elements.
+  SelectionStyle selection_style_ = [](Pixel& pixel) {
+    pixel.inverted ^= true;
+  };
 };
 
 }  // namespace ftxui
diff --git a/src/ftxui/component/container.cpp b/src/ftxui/component/container.cpp
index 1948a47d..410bb9f5 100644
--- a/src/ftxui/component/container.cpp
+++ b/src/ftxui/component/container.cpp
@@ -163,6 +163,7 @@ class VerticalContainer : public ContainerBase {
       return false;
     }
 
+    int old_selected = *selector_;
     if (event.mouse().button == Mouse::WheelUp) {
       MoveSelector(-1);
     }
@@ -171,7 +172,7 @@ class VerticalContainer : public ContainerBase {
     }
     *selector_ = std::max(0, std::min(int(children_.size()) - 1, *selector_));
 
-    return true;
+    return old_selected != *selector_;
   }
 
   Box box_;
diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp
index 45a4fff9..0c179442 100644
--- a/src/ftxui/component/screen_interactive.cpp
+++ b/src/ftxui/component/screen_interactive.cpp
@@ -576,6 +576,18 @@ void ScreenInteractive::ForceHandleCtrlZ(bool force) {
   force_handle_ctrl_z_ = force;
 }
 
+/// @brief Returns the content of the current selection
+std::string ScreenInteractive::GetSelection() {
+  if (!selection_) {
+    return "";
+  }
+  return selection_->GetParts();
+}
+
+void ScreenInteractive::SelectionChange(std::function<void()> callback) {
+  selection_on_change_ = std::move(callback);
+}
+
 /// @brief Return the currently active screen, or null if none.
 // static
 ScreenInteractive* ScreenInteractive::Active() {
@@ -751,6 +763,14 @@ void ScreenInteractive::RunOnce(Component component) {
     ExecuteSignalHandlers();
   }
   Draw(std::move(component));
+
+  if (selection_data_previous_ != selection_data_) {
+    selection_data_previous_ = selection_data_;
+    if (selection_on_change_) {
+      selection_on_change_();
+      Post(Event::Custom);
+    }
+  }
 }
 
 // private
@@ -781,7 +801,9 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
 
       arg.screen_ = this;
 
-      const bool handled = component->OnEvent(arg);
+      bool handled = component->OnEvent(arg);
+
+      handled = HandleSelection(handled, arg);
 
       if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
         RecordSignal(SIGABRT);
@@ -824,6 +846,59 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
   // clang-format on
 }
 
+// private
+bool ScreenInteractive::HandleSelection(bool handled, Event event) {
+  if (handled) {
+    selection_pending_ = nullptr;
+    selection_data_.empty = false;
+    selection_ = nullptr;
+    return true;
+  }
+
+  if (!event.is_mouse()) {
+    return false;
+  }
+
+  auto& mouse = event.mouse();
+  if (mouse.button != Mouse::Left) {
+    return false;
+  }
+
+  if (mouse.motion == Mouse::Pressed) {
+    selection_pending_ = CaptureMouse();
+    selection_data_.start_x = mouse.x;
+    selection_data_.start_y = mouse.y;
+    selection_data_.end_x = mouse.x;
+    selection_data_.end_y = mouse.y;
+    return false;
+  }
+
+  if (!selection_pending_) {
+    return false;
+  }
+
+  if (mouse.motion == Mouse::Moved) {
+    if ((mouse.x != selection_data_.end_x) ||
+        (mouse.y != selection_data_.end_y)) {
+      selection_data_.end_x = mouse.x;
+      selection_data_.end_y = mouse.y;
+      selection_data_.empty = false;
+    }
+
+    return true;
+  }
+
+  if (mouse.motion == Mouse::Released) {
+    selection_pending_ = nullptr;
+    selection_data_.end_x = mouse.x;
+    selection_data_.end_y = mouse.y;
+    selection_data_.empty = false;
+    return true;
+  }
+
+  return false;
+}
+
 // private
 // NOLINTNEXTLINE
 void ScreenInteractive::Draw(Component component) {
@@ -899,7 +974,12 @@ void ScreenInteractive::Draw(Component component) {
 #endif
   previous_frame_resized_ = resized;
 
-  Render(*this, document);
+  selection_ = selection_data_.empty
+                   ? std::make_unique<Selection>()
+                   : std::make_unique<Selection>(
+                         selection_data_.start_x, selection_data_.start_y,  //
+                         selection_data_.end_x, selection_data_.end_y);
+  Render(*this, document.get(), *selection_);
 
   // Set cursor position for user using tools to insert CJK characters.
   {
@@ -988,4 +1068,21 @@ void ScreenInteractive::Signal(int signal) {
 #endif
 }
 
+bool ScreenInteractive::SelectionData::operator==(
+    const ScreenInteractive::SelectionData& other) const {
+  if (empty && other.empty) {
+    return true;
+  }
+  if (empty || other.empty) {
+    return false;
+  }
+  return start_x == other.start_x && start_y == other.start_y &&
+         end_x == other.end_x && end_y == other.end_y;
+}
+
+bool ScreenInteractive::SelectionData::operator!=(
+    const ScreenInteractive::SelectionData& other) const {
+  return !(*this == other);
+}
+
 }  // namespace ftxui.
diff --git a/src/ftxui/dom/flex.cpp b/src/ftxui/dom/flex.cpp
index 2fd3adf6..b6f95c6e 100644
--- a/src/ftxui/dom/flex.cpp
+++ b/src/ftxui/dom/flex.cpp
@@ -80,6 +80,7 @@ class Flex : public Node {
   }
 
   void SetBox(Box box) override {
+    Node::SetBox(box);
     if (children_.empty()) {
       return;
     }
diff --git a/src/ftxui/dom/hbox.cpp b/src/ftxui/dom/hbox.cpp
index 2053e84f..bc08b0fc 100644
--- a/src/ftxui/dom/hbox.cpp
+++ b/src/ftxui/dom/hbox.cpp
@@ -64,6 +64,19 @@ class HBox : public Node {
       x = box.x_max + 1;
     }
   }
+
+  void Select(Selection& selection) override {
+    // If this Node box_ doesn't intersect with the selection, then no
+    // selection.
+    if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
+      return;
+    }
+
+    Selection selection_saturated = selection.SaturateHorizontal(box_);
+    for (auto& child : children_) {
+      child->Select(selection_saturated);
+    }
+  }
 };
 
 }  // namespace
diff --git a/src/ftxui/dom/node.cpp b/src/ftxui/dom/node.cpp
index 5220335d..caaa2b38 100644
--- a/src/ftxui/dom/node.cpp
+++ b/src/ftxui/dom/node.cpp
@@ -1,3 +1,4 @@
+#include <iostream>
 // 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.
@@ -27,6 +28,20 @@ void Node::SetBox(Box box) {
   box_ = box;
 }
 
+/// @brief Compute the selection of an element.
+/// @ingroup dom
+void Node::Select(Selection& selection) {
+  // If this Node box_ doesn't intersect with the selection, then no selection.
+  if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
+    return;
+  }
+
+  // By default we defer the selection to the children.
+  for (auto& child : children_) {
+    child->Select(selection);
+  }
+}
+
 /// @brief Display an element on a ftxui::Screen.
 /// @ingroup dom
 void Node::Render(Screen& screen) {
@@ -42,15 +57,31 @@ void Node::Check(Status* status) {
   status->need_iteration |= (status->iteration == 0);
 }
 
+std::string Node::GetSelectedContent(Selection& selection) {
+  std::string content;
+
+  for (auto& child : children_) {
+    content += child->GetSelectedContent(selection);
+  }
+
+  return content;
+}
+
 /// @brief Display an element on a ftxui::Screen.
 /// @ingroup dom
 void Render(Screen& screen, const Element& element) {
-  Render(screen, element.get());
+  Selection selection;
+  Render(screen, element.get(), selection);
 }
 
 /// @brief Display an element on a ftxui::Screen.
 /// @ingroup dom
 void Render(Screen& screen, Node* node) {
+  Selection selection;
+  Render(screen, node, selection);
+}
+
+void Render(Screen& screen, Node* node, Selection& selection) {
   Box box;
   box.x_min = 0;
   box.y_min = 0;
@@ -73,12 +104,49 @@ void Render(Screen& screen, Node* node) {
     node->Check(&status);
   }
 
-  // Step 3: Draw the element.
+  // Step 3: Selection
+  if (!selection.IsEmpty()) {
+    node->Select(selection);
+  }
+
+  // Step 4: Draw the element.
   screen.stencil = box;
   node->Render(screen);
 
-  // Step 4: Apply shaders
+  // Step 5: Apply shaders
   screen.ApplyShader();
 }
 
+std::string GetNodeSelectedContent(Screen& screen,
+                                   Node* node,
+                                   Selection& selection) {
+  Box box;
+  box.x_min = 0;
+  box.y_min = 0;
+  box.x_max = screen.dimx() - 1;
+  box.y_max = screen.dimy() - 1;
+
+  Node::Status status;
+  node->Check(&status);
+  const int max_iterations = 20;
+  while (status.need_iteration && status.iteration < max_iterations) {
+    // Step 1: Find what dimension this elements wants to be.
+    node->ComputeRequirement();
+
+    // Step 2: Assign a dimension to the element.
+    node->SetBox(box);
+
+    // Check if the element needs another iteration of the layout algorithm.
+    status.need_iteration = false;
+    status.iteration++;
+    node->Check(&status);
+  }
+
+  // Step 3: Selection
+  node->Select(selection);
+
+  // Step 4: get the selected content.
+  return node->GetSelectedContent(selection);
+}
+
 }  // namespace ftxui
diff --git a/src/ftxui/dom/paragraph.cpp b/src/ftxui/dom/paragraph.cpp
index 93f75c2f..482e7e3b 100644
--- a/src/ftxui/dom/paragraph.cpp
+++ b/src/ftxui/dom/paragraph.cpp
@@ -20,6 +20,18 @@ Elements Split(const std::string& the_text) {
   }
   return output;
 }
+
+Element Split(const std::string& paragraph,
+              std::function<Element(std::string)> f) {
+  Elements output;
+  std::stringstream ss(paragraph);
+  std::string line;
+  while (std::getline(ss, line, '\n')) {
+    output.push_back(f(line));
+  }
+  return vbox(std::move(output));
+}
+
 }  // namespace
 
 /// @brief Return an element drawing the paragraph on multiple lines.
@@ -34,18 +46,22 @@ Element paragraph(const std::string& the_text) {
 /// @ingroup dom
 /// @see flexbox.
 Element paragraphAlignLeft(const std::string& the_text) {
-  static const auto config = FlexboxConfig().SetGap(1, 0);
-  return flexbox(Split(the_text), config);
-}
+  return Split(the_text, [](const std::string& line) {
+    static const auto config = FlexboxConfig().SetGap(1, 0);
+    return flexbox(Split(line), config);
+  });
+};
 
 /// @brief Return an element drawing the paragraph on multiple lines, aligned on
 /// the right.
 /// @ingroup dom
 /// @see flexbox.
 Element paragraphAlignRight(const std::string& the_text) {
-  static const auto config =
-      FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::FlexEnd);
-  return flexbox(Split(the_text), config);
+  return Split(the_text, [](const std::string& line) {
+    static const auto config = FlexboxConfig().SetGap(1, 0).Set(
+        FlexboxConfig::JustifyContent::FlexEnd);
+    return flexbox(Split(line), config);
+  });
 }
 
 /// @brief Return an element drawing the paragraph on multiple lines, aligned on
@@ -53,9 +69,11 @@ Element paragraphAlignRight(const std::string& the_text) {
 /// @ingroup dom
 /// @see flexbox.
 Element paragraphAlignCenter(const std::string& the_text) {
-  static const auto config =
-      FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::Center);
-  return flexbox(Split(the_text), config);
+  return Split(the_text, [](const std::string& line) {
+    static const auto config =
+        FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::Center);
+    return flexbox(Split(line), config);
+  });
 }
 
 /// @brief Return an element drawing the paragraph on multiple lines, aligned
@@ -64,11 +82,13 @@ Element paragraphAlignCenter(const std::string& the_text) {
 /// @ingroup dom
 /// @see flexbox.
 Element paragraphAlignJustify(const std::string& the_text) {
-  static const auto config = FlexboxConfig().SetGap(1, 0).Set(
-      FlexboxConfig::JustifyContent::SpaceBetween);
-  Elements words = Split(the_text);
-  words.push_back(text("") | xflex);
-  return flexbox(std::move(words), config);
+  return Split(the_text, [](const std::string& line) {
+    static const auto config = FlexboxConfig().SetGap(1, 0).Set(
+        FlexboxConfig::JustifyContent::SpaceBetween);
+    Elements words = Split(line);
+    words.push_back(text("") | xflex);
+    return flexbox(std::move(words), config);
+  });
 }
 
 }  // namespace ftxui
diff --git a/src/ftxui/dom/selection.cpp b/src/ftxui/dom/selection.cpp
new file mode 100644
index 00000000..6c7f508d
--- /dev/null
+++ b/src/ftxui/dom/selection.cpp
@@ -0,0 +1,168 @@
+// Copyright 2024 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 "ftxui/dom/selection.hpp"  // for Selection
+#include <algorithm>                // for max, min
+
+#include "ftxui/dom/elements.hpp"        // for Element, inverted
+#include "ftxui/dom/node_decorator.hpp"  // for NodeDecorator
+
+namespace ftxui {
+
+namespace {
+class Unselectable : public NodeDecorator {
+ public:
+  using NodeDecorator::NodeDecorator;
+
+  void Select(Selection& selection) override {
+    // Overwrite the select method to do nothing.
+  }
+};
+}  // namespace
+
+/// @brief Create an empty selection.
+Selection::Selection() : empty_(true) {}
+
+/// @brief Create a selection.
+/// @param start_x The x coordinate of the start of the selection.
+/// @param start_y The y coordinate of the start of the selection.
+/// @param end_x The x coordinate of the end of the selection.
+/// @param end_y The y coordinate of the end of the selection.
+Selection::Selection(int start_x, int start_y, int end_x, int end_y)
+    : start_x_(start_x),
+      start_y_(start_y),
+      end_x_(end_x),
+      end_y_(end_y),
+      box_{
+          std::min(start_x, end_x),
+          std::max(start_x, end_x),
+          std::min(start_y, end_y),
+          std::max(start_y, end_y),
+      },
+      empty_(false) {}
+
+Selection::Selection(int start_x,
+                     int start_y,
+                     int end_x,
+                     int end_y,
+                     Selection* parent)
+    : start_x_(start_x),
+      start_y_(start_y),
+      end_x_(end_x),
+      end_y_(end_y),
+      box_{
+          std::min(start_x, end_x),
+          std::max(start_x, end_x),
+          std::min(start_y, end_y),
+          std::max(start_y, end_y),
+      },
+      parent_(parent),
+      empty_(false) {}
+
+/// @brief Get the box of the selection.
+/// @return The box of the selection.
+const Box& Selection::GetBox() const {
+  return box_;
+}
+
+/// @brief Saturate the selection to be inside the box.
+/// This is called by `hbox` to propagate the selection to its children.
+/// @param box The box to saturate the selection in.
+/// @return The saturated selection.
+Selection Selection::SaturateHorizontal(Box box) {
+  int start_x = start_x_;
+  int start_y = start_y_;
+  int end_x = end_x_;
+  int end_y = end_y_;
+
+  const bool start_outside = !box.Contain(start_x, start_y);
+  const bool end_outside = !box.Contain(end_x, end_y);
+  const bool properly_ordered =
+      start_y < end_y || (start_y == end_y && start_x <= end_x);
+  if (properly_ordered) {
+    if (start_outside) {
+      start_x = box.x_min;
+      start_y = box.y_min;
+    }
+    if (end_outside) {
+      end_x = box.x_max;
+      end_y = box.y_max;
+    }
+  } else {
+    if (start_outside) {
+      start_x = box.x_max;
+      start_y = box.y_max;
+    }
+    if (end_outside) {
+      end_x = box.x_min;
+      end_y = box.y_min;
+    }
+  }
+  return Selection(start_x, start_y, end_x, end_y, parent_);
+}
+
+/// @brief Saturate the selection to be inside the box.
+/// This is called by `vbox` to propagate the selection to its children.
+/// @param box The box to saturate the selection in.
+/// @return The saturated selection.
+Selection Selection::SaturateVertical(Box box) {
+  int start_x = start_x_;
+  int start_y = start_y_;
+  int end_x = end_x_;
+  int end_y = end_y_;
+
+  const bool start_outside = !box.Contain(start_x, start_y);
+  const bool end_outside = !box.Contain(end_x, end_y);
+  const bool properly_ordered =
+      start_y < end_y || (start_y == end_y && start_x <= end_x);
+
+  if (properly_ordered) {
+    if (start_outside) {
+      start_x = box.x_min;
+      start_y = box.y_min;
+    }
+    if (end_outside) {
+      end_x = box.x_max;
+      end_y = box.y_max;
+    }
+  } else {
+    if (start_outside) {
+      start_x = box.x_max;
+      start_y = box.y_max;
+    }
+    if (end_outside) {
+      end_x = box.x_min;
+      end_y = box.y_min;
+    }
+  }
+  return Selection(start_x, start_y, end_x, end_y, parent_);
+}
+
+void Selection::AddPart(const std::string& part, int y, int left, int right) {
+  if (parent_ != this) {
+    return parent_->AddPart(part, y, left, right);
+  }
+  [&] {
+    if (parts_.str().empty()) {
+      parts_ << part;
+      return;
+    }
+
+    if (y_ != y) {
+      parts_ << '\n' << part;
+      return;
+    }
+
+    if (x_ == left + 1) {
+      parts_ << part;
+      return;
+    }
+
+    parts_ << part;
+  }();
+  y_ = y;
+  x_ = right;
+}
+
+}  // namespace ftxui
diff --git a/src/ftxui/dom/selection_style.cpp b/src/ftxui/dom/selection_style.cpp
new file mode 100644
index 00000000..c864a79d
--- /dev/null
+++ b/src/ftxui/dom/selection_style.cpp
@@ -0,0 +1,89 @@
+// Copyright 2024 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 make_shared
+#include <utility>  // for move
+
+#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
+#include "ftxui/screen/screen.hpp"       // for Pixel, Screen
+
+namespace ftxui {
+
+namespace {
+
+class SelectionStyleReset : public NodeDecorator {
+ public:
+  SelectionStyleReset(Element child) : NodeDecorator(std::move(child)) {}
+
+  void Render(Screen& screen) final {
+    auto old_style = screen.GetSelectionStyle();
+    screen.SetSelectionStyle([](Pixel& pixel) {});
+    NodeDecorator::Render(screen);
+    screen.SetSelectionStyle(old_style);
+  }
+};
+
+class SelectionStyle : public NodeDecorator {
+ public:
+  SelectionStyle(Element child, std::function<void(Pixel&)> style)
+      : NodeDecorator(std::move(child)), style_(style) {}
+
+  void Render(Screen& screen) final {
+    auto old_style = screen.GetSelectionStyle();
+    auto new_style = [&, old_style](Pixel& pixel) {
+      old_style(pixel);
+      style_(pixel);
+    };
+    screen.SetSelectionStyle(new_style);
+    NodeDecorator::Render(screen);
+    screen.SetSelectionStyle(old_style);
+  }
+
+  std::function<void(Pixel&)> style_;
+};
+
+}  // namespace
+
+/// @brief Reset the selection style of an element.
+/// @param child The input element.
+/// @return The output element with the selection style reset.
+Element selectionStyleReset(Element child) {
+  return std::make_shared<SelectionStyleReset>(std::move(child));
+}
+
+/// @brief Set the background color of an element when selected.
+/// Note that the style is applied on top of the existing style.
+Decorator selectionBackgroundColor(Color foreground) {
+  return selectionStyle([foreground](Pixel& pixel) {  //
+    pixel.background_color = foreground;
+  });
+}
+
+/// @brief Set the foreground color of an element when selected.
+/// Note that the style is applied on top of the existing style.
+Decorator selectionForegroundColor(Color foreground) {
+  return selectionStyle([foreground](Pixel& pixel) {  //
+    pixel.foreground_color = foreground;
+  });
+}
+
+/// @brief Set the color of an element when selected.
+/// @param foreground The color to be applied.
+/// Note that the style is applied on top of the existing style.
+Decorator selectionColor(Color foreground) {
+  return selectionForegroundColor(foreground);
+}
+
+/// @brief Set the style of an element when selected.
+/// @param style The style to be applied.
+/// Note that the style is applied on top of the existing style.
+Decorator selectionStyle(std::function<void(Pixel&)> style) {
+  return [style](Element child) -> Element {
+    return std::make_shared<SelectionStyle>(std::move(child), style);
+  };
+}
+
+}  // namespace ftxui
diff --git a/src/ftxui/dom/selection_test.cpp b/src/ftxui/dom/selection_test.cpp
new file mode 100644
index 00000000..7c48e28e
--- /dev/null
+++ b/src/ftxui/dom/selection_test.cpp
@@ -0,0 +1,224 @@
+// Copyright 2022 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 <gtest/gtest.h>
+#include <csignal>  // for raise, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM
+
+#include "ftxui/component/component.hpp"  // for Input, Renderer, Vertical
+#include "ftxui/component/event.hpp"      // for Event
+#include "ftxui/component/loop.hpp"       // for Loop
+#include "ftxui/component/mouse.hpp"  // for Mouse, Mouse::Left, Mouse::Pressed, Mouse::Released
+#include "ftxui/component/screen_interactive.hpp"
+#include "ftxui/dom/elements.hpp"   // for text
+#include "ftxui/dom/node.hpp"       // for Render
+#include "ftxui/screen/screen.hpp"  // for Screen
+
+// NOLINTBEGIN
+namespace ftxui {
+
+namespace {
+Event MousePressed(int x, int y) {
+  Mouse mouse;
+  mouse.button = Mouse::Left;
+  mouse.motion = Mouse::Pressed;
+  mouse.shift = false;
+  mouse.meta = false;
+  mouse.control = false;
+  mouse.x = x;
+  mouse.y = y;
+  return Event::Mouse("", mouse);
+}
+
+Event MouseReleased(int x, int y) {
+  Mouse mouse;
+  mouse.button = Mouse::Left;
+  mouse.motion = Mouse::Released;
+  mouse.shift = false;
+  mouse.meta = false;
+  mouse.control = false;
+  mouse.x = x;
+  mouse.y = y;
+  return Event::Mouse("", mouse);
+}
+
+Event MouseMove(int x, int y) {
+  Mouse mouse;
+  mouse.button = Mouse::Left;
+  mouse.motion = Mouse::Moved;
+  mouse.shift = false;
+  mouse.meta = false;
+  mouse.control = false;
+  mouse.x = x;
+  mouse.y = y;
+  return Event::Mouse("", mouse);
+}
+
+}  // namespace
+
+TEST(SelectionTest, DefaultSelection) {
+  auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
+  auto screen = ScreenInteractive::FixedSize(20, 1);
+  EXPECT_EQ(screen.GetSelection(), "");
+  Loop loop(&screen, component);
+  screen.PostEvent(MousePressed(3, 1));
+  screen.PostEvent(MouseReleased(10, 1));
+  loop.RunOnce();
+
+  EXPECT_EQ(screen.GetSelection(), "rem ipsu");
+}
+
+TEST(SelectionTest, SelectionChange) {
+  int selectionChangeCounter = 0;
+  auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
+  auto screen = ScreenInteractive::FixedSize(20, 1);
+  screen.SelectionChange([&] { selectionChangeCounter++; });
+
+  Loop loop(&screen, component);
+  loop.RunOnce();
+  EXPECT_EQ(selectionChangeCounter, 0);
+
+  screen.PostEvent(MousePressed(3, 1));
+  loop.RunOnce();
+  EXPECT_EQ(selectionChangeCounter, 0);
+
+  screen.PostEvent(MouseMove(5, 1));
+  loop.RunOnce();
+  EXPECT_EQ(selectionChangeCounter, 1);
+
+  screen.PostEvent(MouseMove(7, 1));
+  loop.RunOnce();
+  EXPECT_EQ(selectionChangeCounter, 2);
+
+  screen.PostEvent(MouseReleased(10, 1));
+  loop.RunOnce();
+  EXPECT_EQ(selectionChangeCounter, 3);
+
+  screen.PostEvent(MouseMove(10, 1));
+  loop.RunOnce();
+  EXPECT_EQ(selectionChangeCounter, 3);
+
+  EXPECT_EQ(screen.GetSelection(), "rem ipsu");
+}
+
+// Check that submitting multiple mouse events quickly doesn't trigger multiple
+// selection change events.
+TEST(SelectionTest, SelectionOnChangeSquashedEvents) {
+  int selectionChangeCounter = 0;
+  auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
+  auto screen = ScreenInteractive::FixedSize(20, 1);
+  screen.SelectionChange([&] { selectionChangeCounter++; });
+
+  Loop loop(&screen, component);
+  loop.RunOnce();
+  EXPECT_EQ(selectionChangeCounter, 0);
+
+  screen.PostEvent(MousePressed(3, 1));
+  screen.PostEvent(MouseMove(5, 1));
+  screen.PostEvent(MouseMove(7, 1));
+  loop.RunOnce();
+  EXPECT_EQ(selectionChangeCounter, 1);
+
+  screen.PostEvent(MouseReleased(10, 1));
+  screen.PostEvent(MouseMove(10, 1));
+  loop.RunOnce();
+  EXPECT_EQ(selectionChangeCounter, 2);
+
+  EXPECT_EQ(screen.GetSelection(), "rem ipsu");
+}
+
+TEST(SelectionTest, StyleSelection) {
+  int selectionChangeCounter = 0;
+
+  auto element = hbox({
+      text("Lorem "),
+      text("ipsum") | selectionColor(Color::Red),
+      text(" dolor"),
+  });
+
+  auto screen = ScreenInteractive::FixedSize(20, 1);
+  Selection selection(2, 0, 9, 0);
+
+  Render(screen, element.get(), selection);
+  for (int i = 0; i < 20; i++) {
+    if (i >= 2 && i <= 9) {
+      EXPECT_EQ(screen.PixelAt(i, 0).inverted, true);
+    } else {
+      EXPECT_EQ(screen.PixelAt(i, 0).inverted, false);
+    }
+
+    if (i >= 6 && i <= 9) {
+      EXPECT_EQ(screen.PixelAt(i, 0).foreground_color, Color::Red);
+    } else {
+      EXPECT_EQ(screen.PixelAt(i, 0).foreground_color, Color::Default);
+    }
+  }
+}
+
+TEST(SelectionTest, VBoxSelection) {
+  auto element = vbox({
+      text("Lorem ipsum dolor"),
+      text("Ut enim ad minim"),
+  });
+
+  auto screen = ScreenInteractive::FixedSize(20, 2);
+
+  Selection selection(2, 0, 2, 1);
+  Render(screen, element.get(), selection);
+
+  EXPECT_EQ(selection.GetParts(), "rem ipsum dolor\nUt ");
+  EXPECT_EQ(screen.ToString(),
+            "Lo\x1B[7mrem ipsum dolor\x1B[27m   \r\n"
+            "\x1B[7mUt \x1B[27menim ad minim    ");
+}
+
+TEST(SelectionTest, VBoxSaturatedSelection) {
+  auto element = vbox({
+      text("Lorem ipsum dolor"),
+      text("Ut enim ad minim"),
+      text("Duis aute irure"),
+  });
+
+  auto screen = ScreenInteractive::FixedSize(20, 3);
+  Selection selection(2, 0, 2, 2);
+  Render(screen, element.get(), selection);
+  EXPECT_EQ(selection.GetParts(), "rem ipsum dolor\nUt enim ad minim\nDui");
+
+  EXPECT_EQ(screen.ToString(),
+            "Lo\x1B[7mrem ipsum dolor\x1B[27m   \r\n"
+            "\x1B[7mUt enim ad minim\x1B[27m    \r\n"
+            "\x1B[7mDui\x1B[27ms aute irure     ");
+}
+
+TEST(SelectionTest, HBoxSelection) {
+  auto element = hbox({
+      text("Lorem ipsum dolor"),
+      text("Ut enim ad minim"),
+  });
+
+  auto screen = ScreenInteractive::FixedSize(40, 1);
+  Selection selection(2, 0, 20, 0);
+  Render(screen, element.get(), selection);
+  EXPECT_EQ(selection.GetParts(), "rem ipsum dolorUt e");
+  EXPECT_EQ(screen.ToString(),
+            "Lo\x1B[7mrem ipsum dolorUt e\x1B[27mnim ad minim       ");
+}
+
+TEST(SelectionTest, HBoxSaturatedSelection) {
+  auto element = hbox({
+      text("Lorem ipsum dolor"),
+      text("Ut enim ad minim"),
+      text("Duis aute irure"),
+  });
+
+  auto screen = ScreenInteractive::FixedSize(60, 1);
+
+  Selection selection(2, 0, 35, 0);
+  Render(screen, element.get(), selection);
+  EXPECT_EQ(selection.GetParts(), "rem ipsum dolorUt enim ad minimDui");
+  EXPECT_EQ(screen.ToString(),
+            "Lo\x1B[7mrem ipsum dolorUt enim ad minimDui\x1B[27ms aute irure   "
+            "         ");
+}
+
+}  // namespace ftxui
+// NOLINTEND
diff --git a/src/ftxui/dom/text.cpp b/src/ftxui/dom/text.cpp
index 228e7142..c416c976 100644
--- a/src/ftxui/dom/text.cpp
+++ b/src/ftxui/dom/text.cpp
@@ -3,8 +3,9 @@
 // the LICENSE file.
 #include <algorithm>  // for min
 #include <memory>     // for make_shared
-#include <string>     // for string, wstring
-#include <utility>    // for move
+#include <sstream>
+#include <string>   // for string, wstring
+#include <utility>  // for move
 
 #include "ftxui/dom/deprecated.hpp"   // for text, vtext
 #include "ftxui/dom/elements.hpp"     // for Element, text, vtext
@@ -26,28 +27,68 @@ class Text : public Node {
   void ComputeRequirement() override {
     requirement_.min_x = string_width(text_);
     requirement_.min_y = 1;
+    has_selection = false;
+  }
+
+  void Select(Selection& selection) override {
+    if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
+      return;
+    }
+
+    Selection selection_saturated = selection.SaturateHorizontal(box_);
+
+    has_selection = true;
+    selection_start_ = selection_saturated.GetBox().x_min;
+    selection_end_ = selection_saturated.GetBox().x_max;
+
+    std::stringstream ss;
+    int x = box_.x_min;
+    for (const auto& cell : Utf8ToGlyphs(text_)) {
+      if (cell == "\n") {
+        continue;
+      }
+      if (selection_start_ <= x && x <= selection_end_) {
+        ss << cell;
+      }
+      x++;
+    }
+    selection.AddPart(ss.str(), box_.y_min, selection_start_, selection_end_);
   }
 
   void Render(Screen& screen) override {
     int x = box_.x_min;
     const int y = box_.y_min;
+
     if (y > box_.y_max) {
       return;
     }
+
     for (const auto& cell : Utf8ToGlyphs(text_)) {
       if (x > box_.x_max) {
-        return;
+        break;
       }
       if (cell == "\n") {
         continue;
       }
       screen.PixelAt(x, y).character = cell;
+
+      if (has_selection) {
+        auto selectionTransform = screen.GetSelectionStyle();
+        if ((x >= selection_start_) && (x <= selection_end_)) {
+          selectionTransform(screen.PixelAt(x, y));
+        }
+      }
+
       ++x;
     }
   }
 
  private:
   std::string text_;
+  bool has_selection = false;
+  int selection_start_ = 0;
+  int selection_end_ = -1;
+  std::function<void(Pixel& pixel)> selectionTransform;
 };
 
 class VText : public Node {
diff --git a/src/ftxui/dom/vbox.cpp b/src/ftxui/dom/vbox.cpp
index 28d885d9..27134568 100644
--- a/src/ftxui/dom/vbox.cpp
+++ b/src/ftxui/dom/vbox.cpp
@@ -64,6 +64,20 @@ class VBox : public Node {
       y = box.y_max + 1;
     }
   }
+
+  void Select(Selection& selection) override {
+    // If this Node box_ doesn't intersect with the selection, then no
+    // selection.
+    if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
+      return;
+    }
+
+    Selection selection_saturated = selection.SaturateVertical(box_);
+
+    for (auto& child : children_) {
+      child->Select(selection_saturated);
+    }
+  }
 };
 }  // namespace
 
diff --git a/src/ftxui/screen/screen.cpp b/src/ftxui/screen/screen.cpp
index 7bd64e29..beb3870a 100644
--- a/src/ftxui/screen/screen.cpp
+++ b/src/ftxui/screen/screen.cpp
@@ -544,4 +544,16 @@ const std::string& Screen::Hyperlink(std::uint8_t id) const {
   return hyperlinks_[id];
 }
 
+/// @brief Return the current selection style.
+/// @see SetSelectionStyle
+const Screen::SelectionStyle& Screen::GetSelectionStyle() const {
+  return selection_style_;
+}
+
+/// @brief Set the current selection style.
+/// @see GetSelectionStyle
+void Screen::SetSelectionStyle(SelectionStyle decorator) {
+  selection_style_ = decorator;
+}
+
 }  // namespace ftxui
diff --git a/tools/license_headers.cpp b/tools/license_headers.cpp
index 851e894a..aa00321f 100644
--- a/tools/license_headers.cpp
+++ b/tools/license_headers.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Arthur Sonzogni. All rights reserved.
+// Copyright 2024 Arthur Sonzogni. All rights reserved.
 // Use of this source code is governed by the MIT license that can be found in
 // the LICENSE file.
 
-- 
GitLab