From 0d47dd19abecff6614f49401267c5856be424716 Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <sonzogniarthur@gmail.com>
Date: Thu, 23 Dec 2021 14:17:33 +0100
Subject: [PATCH] Feature: Canvas (#287)

Draw using braille and block characters on a grid.
---
 CHANGELOG.md                                  |   2 +
 CMakeLists.txt                                |   2 +
 examples/CMakeLists.txt                       |   4 +-
 examples/component/CMakeLists.txt             |   5 +-
 examples/component/canvas_animated.cpp        | 257 ++++++++
 .../{flexbox.cpp => flexbox_gallery.cpp}      |   0
 examples/component/focus.cpp                  |   4 +-
 examples/component/homescreen.cpp             |   3 +-
 examples/dom/CMakeLists.txt                   |   3 +-
 examples/dom/canvas.cpp                       |  50 ++
 examples/dom/flexbox.cpp                      |  38 ++
 examples/index.html                           |   5 +-
 include/ftxui/dom/canvas.hpp                  | 137 ++++
 include/ftxui/dom/elements.hpp                |   3 +
 src/ftxui/dom/canvas.cpp                      | 595 ++++++++++++++++++
 15 files changed, 1098 insertions(+), 10 deletions(-)
 create mode 100644 examples/component/canvas_animated.cpp
 rename examples/component/{flexbox.cpp => flexbox_gallery.cpp} (100%)
 create mode 100644 examples/dom/canvas.cpp
 create mode 100644 examples/dom/flexbox.cpp
 create mode 100644 include/ftxui/dom/canvas.hpp
 create mode 100644 src/ftxui/dom/canvas.cpp

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86c590b0..df59715e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@ unreleased (development)
 ### Features:
 
 #### DOM:
+- Add the `Canvas` class and `ElementFrom('canvas')` function. Together users of
+  the library can draw using braille and block characters.
 - Support `flexbox` dom elements. This is build symmetrically to the HTML one.
   All the following attributes are supported: direction, wrap, justify-content,
   align-items, align-content, gap
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b288f129..4a672bd3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -37,6 +37,7 @@ add_library(screen
 )
 
 add_library(dom
+  include/ftxui/dom/canvas.hpp
   include/ftxui/dom/elements.hpp
   include/ftxui/dom/flexbox_config.hpp
   include/ftxui/dom/node.hpp
@@ -47,6 +48,7 @@ add_library(dom
   src/ftxui/dom/border.cpp
   src/ftxui/dom/box_helper.cpp
   src/ftxui/dom/box_helper.hpp
+  src/ftxui/dom/canvas.cpp
   src/ftxui/dom/clear_under.cpp
   src/ftxui/dom/color.cpp
   src/ftxui/dom/composite_decorator.cpp
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index c30eb671..f5a46b0f 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -1,7 +1,7 @@
 set(EXAMPLES_DIR ${CMAKE_CURRENT_SOURCE_DIR})
 function(example name)
-  add_executable(${name} ${name}.cpp)
-  target_link_libraries(${name} PUBLIC ${DIRECTORY_LIB})
+  add_executable(ftxui_example_${name} ${name}.cpp)
+  target_link_libraries(ftxui_example_${name} PUBLIC ${DIRECTORY_LIB})
   file(RELATIVE_PATH dir ${EXAMPLES_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
   set_property(GLOBAL APPEND PROPERTY FTXUI::EXAMPLES ${dir}/${name})
 endfunction(example)
diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt
index 24ea2bf6..d8a510d4 100644
--- a/examples/component/CMakeLists.txt
+++ b/examples/component/CMakeLists.txt
@@ -1,16 +1,17 @@
 set(DIRECTORY_LIB component)
 
 example(button)
+example(canvas_animated)
 example(checkbox)
 example(checkbox_in_frame)
 example(composition)
 example(dropdown)
-example(flexbox)
+example(flexbox_gallery)
+example(focus)
 example(gallery)
 example(homescreen)
 example(input)
 example(maybe)
-example(focus)
 example(menu)
 example(menu2)
 example(menu_entries)
diff --git a/examples/component/canvas_animated.cpp b/examples/component/canvas_animated.cpp
new file mode 100644
index 00000000..b89280ee
--- /dev/null
+++ b/examples/component/canvas_animated.cpp
@@ -0,0 +1,257 @@
+#include <stddef.h>                // for size_t
+#include <stdio.h>                 // for getchar
+#include <ftxui/dom/elements.hpp>  // for operator|, size, Element, text, hcenter, Decorator, Fit, WIDTH, hflow, window, EQUAL, GREATER_THAN, HEIGHT, bold, border, dim, LESS_THAN
+#include <ftxui/screen/screen.hpp>  // for Full, Screen
+#include <memory>                   // for allocator, shared_ptr
+#include <string>  // for operator+, to_string, char_traits, string
+
+#include "ftxui/component/component.hpp"
+#include "ftxui/component/screen_interactive.hpp"
+
+#include <cmath>
+int main(int argc, const char* argv[]) {
+  using namespace ftxui;
+
+  int mouse_x = 0;
+  int mouse_y = 0;
+
+  // A triangle following the mouse, using braille characters.
+  auto renderer_line_braille = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "Several lines (braille)");
+    c.DrawPointLine(mouse_x, mouse_y, 80, 10, Color::Red);
+    c.DrawPointLine(80, 10, 80, 40, Color::Blue);
+    c.DrawPointLine(80, 40, mouse_x, mouse_y, Color::Green);
+    return ElementFrom(std::move(c));
+  });
+
+  // A triangle following the mouse, using block characters.
+  auto renderer_line_block = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "Several lines (block)");
+    c.DrawBlockLine(mouse_x, mouse_y, 80, 10, Color::Red);
+    c.DrawBlockLine(80, 10, 80, 40, Color::Blue);
+    c.DrawBlockLine(80, 40, mouse_x, mouse_y, Color::Green);
+    return ElementFrom(std::move(c));
+  });
+
+  // A circle following the mouse, using braille characters.
+  auto renderer_circle_braille = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "A circle (braille)");
+    c.DrawPointCircle(mouse_x, mouse_y, 30);
+    return ElementFrom(std::move(c));
+  });
+
+  // A circle following the mouse, using block characters.
+  auto renderer_circle_block = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "A circle (block)");
+    c.DrawBlockCircle(mouse_x, mouse_y, 30);
+    return ElementFrom(std::move(c));
+  });
+
+  // A filled circle following the mouse, using braille characters.
+  auto renderer_circle_filled_braille = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "A circle filled (braille)");
+    c.DrawPointCircleFilled(mouse_x, mouse_y, 30);
+    return ElementFrom(std::move(c));
+  });
+
+  // A filled circle following the mouse, using block characters.
+  auto renderer_circle_filled_block = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "A circle filled (block)");
+    c.DrawBlockCircleFilled(mouse_x, mouse_y, 30);
+    return ElementFrom(std::move(c));
+  });
+
+  // An ellipse following the mouse, using braille characters.
+  auto renderer_ellipse_braille = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "An ellipse (braille)");
+    c.DrawPointEllipse(mouse_x / 2, mouse_y / 2, mouse_x / 2, mouse_y / 2);
+    return ElementFrom(std::move(c));
+  });
+
+  // An ellipse following the mouse, using block characters.
+  auto renderer_ellipse_block = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "An ellipse (block)");
+    c.DrawBlockEllipse(mouse_x / 2, mouse_y / 2, mouse_x / 2, mouse_y / 2);
+    return ElementFrom(std::move(c));
+  });
+
+  // An ellipse following the mouse filled, using braille characters.
+  auto renderer_ellipse_filled_braille = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "A filled ellipse (braille)");
+    c.DrawPointEllipseFilled(mouse_x / 2, mouse_y / 2, mouse_x / 2,
+                              mouse_y / 2);
+    return ElementFrom(std::move(c));
+  });
+
+  // An ellipse following the mouse filled, using block characters.
+  auto renderer_ellipse_filled_block = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0,0, "A filled ellipse (block)");
+    c.DrawBlockEllipseFilled(mouse_x / 2, mouse_y / 2, mouse_x / 2,
+                              mouse_y / 2);
+    c.DrawBlockEllipse(mouse_x / 2, mouse_y / 2, mouse_x / 2, mouse_y / 2);
+    return ElementFrom(std::move(c));
+  });
+
+  // A text following the mouse
+  auto renderer_text = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0, 0, "A piece of text");
+    c.DrawText(mouse_x, mouse_y, "This is a piece of text with effects",
+               [](Pixel& p) {
+                 p.foreground_color = Color::Red;
+                 p.underlined = true;
+                 p.bold = true;
+               });
+    return ElementFrom(std::move(c));
+  });
+
+  auto renderer_plot_1 = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0, 0, "A graph");
+
+    std::vector<int> ys(100);
+    for (int x = 0; x < 100; x++) {
+      float dx = x - mouse_x;
+      float dy = 50;
+      ys[x] = dy + 20 * cos(dx * 0.14) + 10 * sin(dx * 0.42);
+    }
+    for (int x = 1; x < 99; x++)
+      c.DrawPointLine(x, ys[x], x + 1, ys[x + 1]);
+
+    return ElementFrom(std::move(c));
+  });
+
+  auto renderer_plot_2 = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0, 0, "A symmetrical graph filled");
+    std::vector<int> ys(100);
+    for (int x = 0; x < 100; x++) {
+      ys[x] = 30 +  //
+              10 * cos(x * 0.2 - mouse_x * 0.05) + //
+              5 * sin(x * 0.4) + //
+              5 * sin(x * 0.3 - mouse_y * 0.05); //
+    }
+    for (int x = 0; x < 100; x++) {
+      c.DrawPointLine(x, 50+ys[x], x, 50-ys[x], Color::Red);
+    }
+
+    return ElementFrom(std::move(c));
+  });
+
+  auto renderer_plot_3 = Renderer([&] {
+    auto c = Canvas(100, 100);
+    c.DrawText(0, 0, "A 2D gaussian plot");
+    int size = 15;
+
+    // mouse_x = 5mx + 3*my 
+    // mouse_y = 0mx + -5my + 90
+    float my = (mouse_y - 90) / -5.f;
+    float mx = (mouse_x - 3 * my) / 5.f;
+    std::vector<std::vector<float>> ys(size, std::vector<float>(size));
+    for (int y = 0; y < size; y++) {
+      for (int x = 0; x < size; x++) {
+        float dx = x-mx;
+        float dy = y-my;
+        ys[y][x] = -1.5 + 3.0 * std::exp(-0.2f * (dx*dx+dy*dy));
+      }
+    }
+    for (int y = 0; y < size; y++) {
+      for (int x = 0; x < size; x++) {
+        if (x != 0) {
+          c.DrawPointLine(
+              5 * (x - 1) + 3 * (y - 0), 90 - 5 * (y - 0) - 5 * ys[y][x - 1],
+              5 * (x - 0) + 3 * (y - 0), 90 - 5 * (y - 0) - 5 * ys[y][x]);
+        }
+        if (y != 0) {
+          c.DrawPointLine(
+              5 * (x - 0) + 3 * (y - 1), 90 - 5 * (y - 1) - 5 * ys[y - 1][x],
+              5 * (x - 0) + 3 * (y - 0), 90 - 5 * (y - 0) - 5 * ys[y][x]);
+        }
+      }
+    }
+
+    return ElementFrom(std::move(c));
+  });
+
+
+  int selected_tab = 0;
+  auto tab = Container::Tab({
+      renderer_line_braille,
+      renderer_line_block,
+      renderer_circle_braille,
+      renderer_circle_block,
+      renderer_circle_filled_braille,
+      renderer_circle_filled_block,
+      renderer_ellipse_braille,
+      renderer_ellipse_block,
+      renderer_ellipse_filled_braille,
+      renderer_ellipse_filled_block,
+
+      renderer_plot_1,
+      renderer_plot_2,
+      renderer_plot_3,
+
+      renderer_text,
+  }, &selected_tab);
+
+  // This capture the last mouse position.
+  auto tab_with_mouse = CatchEvent(tab, [&](Event e) {
+    if (e.is_mouse()) {
+      mouse_x = (e.mouse().x - 1) * 2;
+      mouse_y = (e.mouse().y - 1) * 4;
+    }
+    return false;
+  });
+
+  std::vector<std::string> tab_titles = {
+      "line (braille)",
+      "line (block)",
+      "circle (braille)",
+      "circle (block)",
+      "circle filled (braille)",
+      "circle filled (block)",
+      "ellipse (braille)",
+      "ellipse (block)",
+      "ellipse filled (braille)",
+      "ellipse filled (block)",
+      "plot_1 simple",
+      "plot_2 filled",
+      "plot_3 3D",
+      "text",
+  };
+  auto tab_toggle = Menu(&tab_titles, &selected_tab);
+
+  auto component = Container::Horizontal({
+      tab_with_mouse,
+      tab_toggle,
+  });
+
+  // Add some separator to decorate the whole component:
+  auto component_renderer = Renderer(component, [&] {
+    return hbox({
+               tab_with_mouse->Render(),
+               separator(),
+               tab_toggle->Render(),
+           }) |
+           border;
+  });
+
+  auto screen = ScreenInteractive::FitComponent();
+  screen.Loop(component_renderer);
+
+  return 0;
+}
+
+// Copyright 2021 Arthur Sonzogni. All rights reserved.
+// Use of this source code is governed by the MIT license that can be found in
+// the LICENSED file.
diff --git a/examples/component/flexbox.cpp b/examples/component/flexbox_gallery.cpp
similarity index 100%
rename from examples/component/flexbox.cpp
rename to examples/component/flexbox_gallery.cpp
diff --git a/examples/component/focus.cpp b/examples/component/focus.cpp
index c1a5c563..50e8d600 100644
--- a/examples/component/focus.cpp
+++ b/examples/component/focus.cpp
@@ -37,8 +37,8 @@ int main(int argc, const char* argv[]) {
   float focus_x = 0.0f;
   float focus_y = 0.0f;
 
-  auto slider_x = Slider("x", &focus_x, 0.f, 1.f, 0.05f);
-  auto slider_y = Slider("y", &focus_y, 0.f, 1.f, 0.05f);
+  auto slider_x = Slider("x", &focus_x, 0.f, 1.f, 0.01f);
+  auto slider_y = Slider("y", &focus_y, 0.f, 1.f, 0.01f);
 
   auto renderer = Renderer(
       Container::Vertical({
diff --git a/examples/component/homescreen.cpp b/examples/component/homescreen.cpp
index 70c206bd..0fbdd2b7 100644
--- a/examples/component/homescreen.cpp
+++ b/examples/component/homescreen.cpp
@@ -406,8 +406,7 @@ int main(int argc, const char* argv[]) {
                    make_box(6, 3),
                }),
            }) |
-           // vscroll_indicator | yflex;
-           yflex | vscroll_indicator;
+           vscroll_indicator | yframe | flex;
   });
 
   auto paragraph_renderer_right = Renderer([] {
diff --git a/examples/dom/CMakeLists.txt b/examples/dom/CMakeLists.txt
index 5428e212..f7c720c9 100644
--- a/examples/dom/CMakeLists.txt
+++ b/examples/dom/CMakeLists.txt
@@ -7,11 +7,11 @@ example(color_info_palette256)
 example(color_truecolor_HSV)
 example(color_truecolor_RGB)
 example(dbox)
+example(canvas)
 example(gauge)
 example(graph)
 example(gridbox)
 example(hflow)
-example(vflow)
 example(html_like)
 example(package_manager)
 example(paragraph)
@@ -28,4 +28,5 @@ example(style_inverted)
 example(style_underlined)
 example(table)
 example(vbox_hbox)
+example(vflow)
 example(window)
diff --git a/examples/dom/canvas.cpp b/examples/dom/canvas.cpp
new file mode 100644
index 00000000..9f2fca24
--- /dev/null
+++ b/examples/dom/canvas.cpp
@@ -0,0 +1,50 @@
+#include <stddef.h>                // for size_t
+#include <stdio.h>                 // for getchar
+#include <ftxui/dom/elements.hpp>  // for operator|, size, Element, text, hcenter, Decorator, Fit, WIDTH, hflow, window, EQUAL, GREATER_THAN, HEIGHT, bold, border, dim, LESS_THAN
+#include <ftxui/screen/screen.hpp>  // for Full, Screen
+#include <memory>                   // for allocator, shared_ptr
+#include <string>  // for operator+, to_string, char_traits, string
+
+#include "ftxui/dom/flexbox_config.hpp"  // for ftxui
+#include "ftxui/dom/node.hpp"            // for Render
+
+#include <cmath>
+int main(int argc, const char* argv[]) {
+  using namespace ftxui;
+
+  auto canvas = Canvas(100, 100);
+
+  canvas.DrawText(0, 0, "This is a canvas", [](Pixel& p) -> void {
+    p.foreground_color = Color::Red;
+    p.underlined = true;
+  });
+
+  // Triangle:
+  canvas.DrawPointLine(10, 10, 80, 10, Color::Red);
+  canvas.DrawPointLine(80, 10, 80, 40, Color::Blue);
+  canvas.DrawPointLine(80, 40, 10, 10, Color::Green);
+
+  // Circle, not filled and filled:
+  canvas.DrawPointCircle(30, 50, 20);
+  canvas.DrawPointCircleFilled(40, 40, 10);
+
+  // Plot a function:
+  std::vector<int> ys(100);
+  for (int x = 0; x < 100; x++)
+    ys[x] = 80 + 20 * cos(x * 0.2);
+  for (int x = 0; x < 99; x++) 
+    canvas.DrawPointLine(x, ys[x], x + 1, ys[x + 1], Color::Red);
+
+  auto document = ElementFrom(&canvas) | border;
+
+  auto screen = Screen::Create(Dimension::Fit(document));
+  Render(screen, document);
+  screen.Print();
+  getchar();
+
+  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/examples/dom/flexbox.cpp b/examples/dom/flexbox.cpp
new file mode 100644
index 00000000..deac3927
--- /dev/null
+++ b/examples/dom/flexbox.cpp
@@ -0,0 +1,38 @@
+#include <stddef.h>                // for size_t
+#include <stdio.h>                 // for getchar
+#include <ftxui/dom/elements.hpp>  // for operator|, size, Element, text, hcenter, Decorator, Fit, WIDTH, hflow, window, EQUAL, GREATER_THAN, HEIGHT, bold, border, dim, LESS_THAN
+#include <ftxui/screen/screen.hpp>  // for Full, Screen
+#include <memory>                   // for allocator, shared_ptr
+#include <string>  // for operator+, to_string, char_traits, string
+
+#include "ftxui/dom/flexbox_config.hpp"  // for ftxui
+#include "ftxui/dom/node.hpp"            // for Render
+
+int main(int argc, const char* argv[]) {
+  using namespace ftxui;
+
+  auto image = Canvas(100, 100);
+
+  auto document = vbox({
+    make_box("header"),
+    hbox({
+      make_box("left side"),
+      make_box("center") | flex,
+      make_box("right side"),
+    }) | flex,
+    make_box("footer")
+  });
+
+  //auto screen = Screen::Create(Dimension::Full(), Dimension::Fit(document));
+  //auto screen = Screen::Create(Dimension::Fit(document));
+  auto screen = Screen::Create(Dimension::Full());
+  Render(screen, document);
+  screen.Print();
+  getchar();
+
+  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/examples/index.html b/examples/index.html
index fe903b62..13212b39 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -71,7 +71,10 @@
       postRun: [],
       onRuntimeInitialized: () => {},
     };
-    document.querySelector("#example_script").src = example + '.js';
+
+    const words = example.split('/')
+    words[1] = "ftxui_example_" + words[1] + ".js"
+    document.querySelector("#example_script").src = words.join('/');
   </script>
 
   <style>
diff --git a/include/ftxui/dom/canvas.hpp b/include/ftxui/dom/canvas.hpp
new file mode 100644
index 00000000..20de184c
--- /dev/null
+++ b/include/ftxui/dom/canvas.hpp
@@ -0,0 +1,137 @@
+#ifndef FTXUI_DOM_CANVAS_HPP
+#define FTXUI_DOM_CANVAS_HPP
+
+#include "ftxui/screen/color.hpp"
+#include "ftxui/screen/screen.hpp"
+#include <unordered_map>
+#include <functional>
+
+namespace ftxui {
+
+struct Canvas {
+ public:
+  Canvas() {}
+  Canvas(int width, int height);
+
+  // Getters:
+  int width() const { return width_; }
+  int height() const { return height_; }
+  Pixel GetPixel(int x, int y) const;
+
+  using Stylizer = std::function<void(Pixel&)>;
+
+  // Draws using braille characters --------------------------------------------
+  void DrawPointOn(int x, int y);
+  void DrawPointOff(int x, int y);
+  void DrawPointToggle(int x, int y);
+  void DrawPoint(int x, int y, bool value);
+  void DrawPoint(int x, int y, bool value, const Stylizer& s);
+  void DrawPoint(int x, int y, bool value, const Color& color);
+  void DrawPointLine(int x1, int y1, int x2, int y2);
+  void DrawPointLine(int x1, int y1, int x2, int y2, const Stylizer& s);
+  void DrawPointLine(int x1, int y1, int x2, int y2, const Color& color);
+  void DrawPointCircle(int x, int y, int radius);
+  void DrawPointCircle(int x, int y, int radius, const Stylizer& s);
+  void DrawPointCircle(int x, int y, int radius, const Color& color);
+  void DrawPointCircleFilled(int x, int y, int radius);
+  void DrawPointCircleFilled(int x,
+                             int y,
+                             int radius,
+                             const Stylizer& s);
+  void DrawPointCircleFilled(int x, int y, int radius, const Color& color);
+  void DrawPointEllipse(int x, int y, int r1, int r2);
+  void DrawPointEllipse(int x, int y, int r1, int r2, const Color& color);
+  void DrawPointEllipse(int x, int y, int r1, int r2, const Stylizer& s);
+  void DrawPointEllipseFilled(int x, int y, int r1, int r2);
+  void DrawPointEllipseFilled(int x,
+                               int y,
+                               int r1,
+                               int r2,
+                               const Color& color);
+  void DrawPointEllipseFilled(int x,
+                               int y,
+                               int r1,
+                               int r2,
+                               const Stylizer& s);
+
+  // Draw using box characters -------------------------------------------------
+  // Block are of size 1x2. y is considered to be a multiple of 2.
+  void DrawBlockOn(int x, int y);
+  void DrawBlockOff(int x, int y);
+  void DrawBlockToggle(int x, int y);
+  void DrawBlock(int x, int y, bool value);
+  void DrawBlock(int x, int y, bool value, const Stylizer& s);
+  void DrawBlock(int x, int y, bool value, const Color& color);
+  void DrawBlockLine(int x1, int y1, int x2, int y2);
+  void DrawBlockLine(int x1, int y1, int x2, int y2, const Stylizer& s);
+  void DrawBlockLine(int x1, int y1, int x2, int y2, const Color& color);
+  void DrawBlockCircle(int x1, int y1, int radius);
+  void DrawBlockCircle(int x1, int y1, int radius, const Stylizer& s);
+  void DrawBlockCircle(int x1, int y1, int radius, const Color& color);
+  void DrawBlockCircleFilled(int x1, int y1, int radius);
+  void DrawBlockCircleFilled(int x1, int y1, int radius, const Stylizer& s);
+  void DrawBlockCircleFilled(int x1, int y1, int radius, const Color& color);
+  void DrawBlockEllipse(int x1, int y1, int r1, int r2);
+  void DrawBlockEllipse(int x1, int y1, int r1, int r2, const Stylizer& s);
+  void DrawBlockEllipse(int x1, int y1, int r1, int r2, const Color& color);
+  void DrawBlockEllipseFilled(int x1, int y1, int r1, int r2);
+  void DrawBlockEllipseFilled(int x1,
+                               int y1,
+                               int r1,
+                               int r2,
+                               const Stylizer& s);
+  void DrawBlockEllipseFilled(int x1,
+                               int y1,
+                               int r1,
+                               int r2,
+                               const Color& color);
+
+  // Draw using normal characters ----------------------------------------------
+  // Draw using character of size 2x4 at position (x,y)
+  // x is considered to be a multiple of 2.
+  // y is considered to be a multiple of 4.
+  void DrawText(int x, int y, const std::string& value);
+  void DrawText(int x, int y, const std::string& value, const Color& color);
+  void DrawText(int x,
+                int y,
+                const std::string& value,
+                const Stylizer& style);
+
+  // Decorator:
+  // x is considered to be a multiple of 2.
+  // y is considered to be a multiple of 4.
+  void Style(int x, int y, const Stylizer& style);
+
+ private:
+  bool IsIn(int x, int y) const {
+    return x >= 0 && x < width_ && y >= 0 && y < height_;
+  }
+  enum CellType {
+    kBraille,
+    kBlock,
+    kText,
+  };
+  struct Cell {
+    CellType type = kText;
+    Pixel content;
+  };
+  struct XY {
+    int x;
+    int y;
+    bool operator==(const XY& other) const {
+      return x == other.x && y == other.y;
+    }
+  };
+
+  struct XYHash {
+    size_t operator()(const XY& xy) const { return xy.x * 1024 + xy.y; }
+  };
+
+  int width_ = 0;
+  int height_ = 0;
+  std::unordered_map<XY, Cell, XYHash> storage_;
+};
+
+}  // namespace ftxui
+
+#endif // FTXUI_DOM_CANVAS_HPP
diff --git a/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp
index d3e53ed0..5f378229 100644
--- a/include/ftxui/dom/elements.hpp
+++ b/include/ftxui/dom/elements.hpp
@@ -4,12 +4,14 @@
 #include <functional>
 #include <memory>
 
+#include "ftxui/dom/canvas.hpp"
 #include "ftxui/dom/flexbox_config.hpp"
 #include "ftxui/dom/node.hpp"
 #include "ftxui/screen/box.hpp"
 #include "ftxui/screen/color.hpp"
 #include "ftxui/screen/screen.hpp"
 #include "ftxui/screen/terminal.hpp"
+#include "ftxui/util/ref.hpp"
 
 namespace ftxui {
 class Node;
@@ -57,6 +59,7 @@ Element paragraphAlignCenter(std::string text);
 Element paragraphAlignJustify(std::string text);
 Element graph(GraphFunction);
 Element emptyElement();
+Element ElementFrom(ConstRef<Canvas>);
 
 // -- Decorator ---
 Element bold(Element);
diff --git a/src/ftxui/dom/canvas.cpp b/src/ftxui/dom/canvas.cpp
new file mode 100644
index 00000000..e25b3cb5
--- /dev/null
+++ b/src/ftxui/dom/canvas.cpp
@@ -0,0 +1,595 @@
+#include "ftxui/dom/canvas.hpp"
+
+#include <map>
+
+#include "ftxui/dom/elements.hpp"
+#include "ftxui/screen/screen.hpp"
+
+namespace ftxui {
+
+namespace {
+
+// Base UTF8 pattern:
+// 11100010 10100000 10000000 // empty
+
+// Pattern for the individuel dots:
+// ┌──────┬───────┐
+// │dot1  │ dot4  │
+// ├──────┼───────┤
+// │dot2  │ dot5  │
+// ├──────┼───────┤
+// │dot3  │ dot6  │
+// ├──────┼───────┤
+// │dot0-1│ dot0-2│
+// └──────┴───────┘
+// 11100010 10100000 10000001 // dot1
+// 11100010 10100000 10000010 // dot2
+// 11100010 10100000 10000100 // dot3
+// 11100010 10100001 10000000 // dot0-1
+// 11100010 10100000 10001000 // dot4
+// 11100010 10100000 10010000 // dot5
+// 11100010 10100000 10100000 // dot6
+// 11100010 10100010 10000000 // dot0-2
+
+uint8_t g_map_braille[2][4][2] = {
+    {
+        {0b00000000, 0b00000001},  // dot1
+        {0b00000000, 0b00000010},  // dot2
+        {0b00000000, 0b00000100},  // dot3
+        {0b00000001, 0b00000000},  // dot0-1
+    },
+    {
+        {0b00000000, 0b00001000},  // dot4
+        {0b00000000, 0b00010000},  // dot5
+        {0b00000000, 0b00100000},  // dot6
+        {0b00000010, 0b00000000},  // dot0-2
+    },
+};
+
+std::vector<std::string> g_map_block = {
+    " ", "▘", "▖", "▌", "▝", "▀", "▞", "▛",
+    "▗", "▚", "▄", "▙", "▐", "▜", "▟", "█",
+};
+
+const std::map<std::string, uint8_t> g_map_block_inversed = {
+    {" ", 0b0000}, {"▘", 0b0001}, {"▖", 0b0010}, {"▌", 0b0011},
+    {"▝", 0b0100}, {"▀", 0b0101}, {"▞", 0b0110}, {"▛", 0b0111},
+    {"▗", 0b1000}, {"▚", 0b1001}, {"▄", 0b1010}, {"▙", 0b1011},
+    {"▐", 0b1100}, {"▜", 0b1101}, {"▟", 0b1110}, {"█", 0b1111},
+};
+
+}  // namespace
+
+Canvas::Canvas(int width, int height)
+    : width_(width), height_(height), storage_(width_ * height_ / 8) {}
+
+Pixel Canvas::GetPixel(int x, int y) const {
+  auto it = storage_.find(XY{x / 2, y / 4});
+  return (it == storage_.end()) ? Pixel{} : it->second.content;
+}
+
+void Canvas::DrawPoint(int x, int y, bool value) {
+  DrawPoint(x, y, value, [](Pixel&) {});
+}
+
+void Canvas::DrawPoint(int x, int y, bool value, const Color& color) {
+  DrawPoint(x, y, value, [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawPoint(int x, int y, bool value, const Stylizer& style) {
+  Style(x, y, style);
+  if (value)
+    DrawPointOn(x, y);
+  else
+    DrawPointOff(x, y);
+}
+
+void Canvas::DrawPointOn(int x, int y) {
+  if (!IsIn(x,y))
+    return;
+  Cell& cell = storage_[XY{x / 2, y / 4}];
+  if (cell.type != CellType::kBraille) {
+    cell.content.character = "⠀";  // 3 bytes.
+    cell.type = CellType::kBraille;
+  }
+
+  cell.content.character[1] |= g_map_braille[x % 2][y % 4][0];
+  cell.content.character[2] |= g_map_braille[x % 2][y % 4][1];
+}
+
+void Canvas::DrawPointOff(int x, int y) {
+  if (!IsIn(x,y))
+    return;
+  Cell& cell = storage_[XY{x / 2, y / 4}];
+  if (cell.type != CellType::kBraille) {
+    cell.content.character = "⠀";  // 3 byt
+    cell.type = CellType::kBraille;
+  }
+
+  cell.content.character[1] &= ~(g_map_braille[x % 2][y % 4][0]);
+  cell.content.character[2] &= ~(g_map_braille[x % 2][y % 4][1]);
+}
+
+void Canvas::DrawPointToggle(int x, int y) {
+  if (!IsIn(x,y))
+    return;
+  Cell& cell = storage_[XY{x / 2, y / 4}];
+  if (cell.type != CellType::kBraille) {
+    cell.content.character = "⠀";  // 3 byt
+    cell.type = CellType::kBraille;
+  }
+
+  cell.content.character[1] ^= g_map_braille[x % 2][y % 4][0];
+  cell.content.character[2] ^= g_map_braille[x % 2][y % 4][1];
+}
+
+void Canvas::DrawPointLine(int x1, int y1, int x2, int y2) {
+  DrawPointLine(x1, y1, x2, y2, [](Pixel&) {});
+}
+
+void Canvas::DrawPointLine(int x1, int y1, int x2, int y2, const Color& color) {
+  DrawPointLine(x1, y1, x2, y2,
+                [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawPointLine(int x1,
+                           int y1,
+                           int x2,
+                           int y2,
+                           const Stylizer& style) {
+  const int dx = std::abs(x2 - x1);
+  const int dy = std::abs(y2 - y1);
+  const int sx = x1 < x2 ? 1 : -1;
+  const int sy = y1 < y2 ? 1 : -1;
+  const int length = std::max(dx, dy);
+
+  if (!IsIn(x1, y1) && !IsIn(x2, y2))
+    return;
+  if (dx + dx > width_ * height_)
+    return;
+
+  int error = dx - dy;
+  for (int i = 0; i < length; ++i) {
+    DrawPoint(x1, y1, true, style);
+    if (2 * error >= -dy) {
+      error -= dy;
+      x1 += sx;
+    }
+    if (2 * error <= dx) {
+      error += dx;
+      y1 += sy;
+    }
+  }
+  DrawPoint(x2, y2, true, style);
+}
+
+void Canvas::DrawPointCircle(int x1, int y1, int radius) {
+  DrawPointCircle(x1, y1, radius, [](Pixel&) {});
+}
+
+void Canvas::DrawPointCircle(int x1, int y1, int radius, const Color& color) {
+  DrawPointCircle(x1, y1, radius,
+                  [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawPointCircle(int x1,
+                             int y1,
+                             int radius,
+                             const Stylizer& style) {
+  DrawPointEllipse(x1, y1, radius, radius, style);
+}
+
+void Canvas::DrawPointCircleFilled(int x1, int y1, int radius) {
+  DrawPointCircleFilled(x1, y1, radius, [](Pixel&) {});
+}
+
+void Canvas::DrawPointCircleFilled(int x1,
+                                   int y1,
+                                   int radius,
+                                   const Color& color) {
+  DrawPointCircleFilled(x1, y1, radius,
+                        [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawPointCircleFilled(int x1,
+                                   int y1,
+                                   int radius,
+                                   const Stylizer& style) {
+  DrawPointEllipseFilled(x1, y1, radius, radius, style);
+}
+
+void Canvas::DrawPointEllipse(int x1, int y1, int r1, int r2) {
+  DrawPointEllipse(x1, y1, r1, r2, [](Pixel&) {});
+}
+
+void Canvas::DrawPointEllipse(int x1,
+                               int y1,
+                               int r1,
+                               int r2,
+                               const Color& color) {
+  DrawPointEllipse(x1, y1, r1, r2,
+                    [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawPointEllipse(int x1,
+                               int y1,
+                               int r1,
+                               int r2,
+                               const Stylizer& s) {
+  int x = -r1;
+  int y = 0;
+  int e2 = r2;
+  int dx = (1 + 2 * x) * e2 * e2;
+  int dy = x * x;
+  int err = dx + dy;
+
+  do {
+    DrawPoint(x1 - x, y1 + y, true, s);
+    DrawPoint(x1 + x, y1 + y, true, s);
+    DrawPoint(x1 + x, y1 - y, true, s);
+    DrawPoint(x1 - x, y1 - y, true, s);
+    e2 = 2 * err;
+    if (e2 >= dx) {
+      x++;
+      err += dx += 2 * r2 * r2;
+    }
+    if (e2 <= dy) {
+      y++;
+      err += dy += 2 * r1 * r1;
+    }
+  } while (x <= 0);
+
+  while (y++ < r2) {
+    DrawPoint(x1, y1 + y, true, s);
+    DrawPoint(x1, y1 - y, true, s);
+  }
+}
+
+void Canvas::DrawPointEllipseFilled(int x1, int y1, int r1, int r2) {
+  DrawPointEllipseFilled(x1, y1, r1, r2, [](Pixel&) {});
+}
+
+void Canvas::DrawPointEllipseFilled(int x1,
+                                     int y1,
+                                     int r1,
+                                     int r2,
+                                     const Color& color) {
+  DrawPointEllipseFilled(x1, y1, r1, r2,
+                          [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawPointEllipseFilled(int x1,
+                                     int y1,
+                                     int r1,
+                                     int r2,
+                                     const Stylizer& s) {
+  int x = -r1;
+  int y = 0;
+  int e2 = r2;
+  int dx = (1 + 2 * x) * e2 * e2;
+  int dy = x * x;
+  int err = dx + dy;
+
+  do {
+    for (int xx = x1 + x; xx <= x1 - x; ++xx) {
+      DrawPoint(xx, y1 + y, true, s);
+      DrawPoint(xx, y1 - y, true, s);
+    }
+    e2 = 2 * err;
+    if (e2 >= dx) {
+      x++;
+      err += dx += 2 * (long)r2 * r2;
+    }
+    if (e2 <= dy) {
+      y++;
+      err += dy += 2 * (long)r1 * r1;
+    }
+  } while (x <= 0);
+
+  while (y++ < r2) {
+    for (int yy = y1 - y; yy <= y1 + y; ++yy) {
+      DrawPoint(x1, yy, true, s);
+    }
+  }
+}
+
+void Canvas::DrawBlock(int x, int y, bool value) {
+  DrawBlock(x, y, value, [](Pixel&) {});
+}
+
+void Canvas::DrawBlock(int x, int y, bool value, const Color& color) {
+  DrawBlock(x, y, value, [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawBlock(int x, int y, bool value, const Stylizer& style) {
+  Style(x, y, style);
+  if (value)
+    DrawBlockOn(x, y);
+  else
+    DrawBlockOff(x, y);
+}
+
+void Canvas::DrawBlockOn(int x, int y) {
+  if (!IsIn(x,y))
+    return;
+  y /= 2;
+  Cell& cell = storage_[XY{x / 2, y / 2}];
+  if (cell.type != CellType::kBlock) {
+    cell.content.character = " ";
+    cell.type = CellType::kBlock;
+  }
+
+  int bit = (x % 2) * 2 + y % 2;
+  uint8_t value = g_map_block_inversed.at(cell.content.character);
+  value |= 1 << bit;
+  cell.content.character = g_map_block[value];
+}
+
+void Canvas::DrawBlockOff(int x, int y) {
+  if (!IsIn(x,y))
+    return;
+  Cell& cell = storage_[XY{x / 2, y / 4}];
+  if (cell.type != CellType::kBlock) {
+    cell.content.character = " ";
+    cell.type = CellType::kBlock;
+  }
+  y /= 2;
+
+  int bit = (y % 2) * 2 + x % 2;
+  uint8_t value = g_map_block_inversed.at(cell.content.character);
+  value &= ~(1 << bit);
+  cell.content.character = g_map_block[value];
+}
+
+void Canvas::DrawBlockToggle(int x, int y) {
+  if (!IsIn(x,y))
+    return;
+  Cell& cell = storage_[XY{x / 2, y / 4}];
+  if (cell.type != CellType::kBlock) {
+    cell.content.character = " ";
+    cell.type = CellType::kBlock;
+  }
+  y /= 2;
+
+  int bit = (y % 2) * 2 + x % 2;
+  uint8_t value = g_map_block_inversed.at(cell.content.character);
+  value ^= 1 << bit;
+  cell.content.character = g_map_block[value];
+}
+
+void Canvas::DrawBlockLine(int x1, int y1, int x2, int y2) {
+  DrawBlockLine(x1, y1, x2, y2, [](Pixel&) {});
+}
+
+void Canvas::DrawBlockLine(int x1, int y1, int x2, int y2, const Color& color) {
+  DrawBlockLine(x1, y1, x2, y2,
+                [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawBlockLine(int x1,
+                           int y1,
+                           int x2,
+                           int y2,
+                           const Stylizer& style) {
+  y1 /= 2;
+  y2 /= 2;
+
+  const int dx = std::abs(x2 - x1);
+  const int dy = std::abs(y2 - y1);
+  const int sx = x1 < x2 ? 1 : -1;
+  const int sy = y1 < y2 ? 1 : -1;
+  const int length = std::max(dx, dy);
+
+  if (!IsIn(x1, y1) && !IsIn(x2, y2))
+    return;
+  if (dx + dx > width_ * height_)
+    return;
+
+  int error = dx - dy;
+  for (int i = 0; i < length; ++i) {
+    DrawBlock(x1, y1 * 2, true, style);
+    if (2 * error >= -dy) {
+      error -= dy;
+      x1 += sx;
+    }
+    if (2 * error <= dx) {
+      error += dx;
+      y1 += sy;
+    }
+  }
+  DrawBlock(x2, y2 * 2, true, style);
+}
+
+void Canvas::DrawBlockCircle(int x1, int y1, int radius) {
+  DrawBlockCircle(x1, y1, radius, [](Pixel&) {});
+}
+
+void Canvas::DrawBlockCircle(int x1, int y1, int radius, const Color& color) {
+  DrawBlockCircle(x1, y1, radius,
+                  [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawBlockCircle(int x1,
+                             int y1,
+                             int radius,
+                             const Stylizer& style) {
+  DrawBlockEllipse(x1, y1, radius, radius, style);
+}
+
+void Canvas::DrawBlockCircleFilled(int x1, int y1, int radius) {
+  DrawBlockCircleFilled(x1, y1, radius, [](Pixel&) {});
+}
+
+void Canvas::DrawBlockCircleFilled(int x1,
+                                   int y1,
+                                   int radius,
+                                   const Color& color) {
+  DrawBlockCircleFilled(x1, y1, radius,
+                        [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawBlockCircleFilled(int x1,
+                                   int y1,
+                                   int radius,
+                                   const Stylizer& s) {
+  DrawBlockEllipseFilled(x1, y1, radius, radius, s);
+}
+
+void Canvas::DrawBlockEllipse(int x1, int y1, int r1, int r2) {
+  DrawBlockEllipse(x1, y1, r1, r2, [](Pixel&) {});
+}
+
+void Canvas::DrawBlockEllipse(int x1,
+                               int y1,
+                               int r1,
+                               int r2,
+                               const Color& color) {
+  DrawBlockEllipse(x1, y1, r1, r2,
+                    [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawBlockEllipse(int x1,
+                               int y1,
+                               int r1,
+                               int r2,
+                               const Stylizer& s) {
+  y1 /= 2;
+  r2 /= 2;
+  int x = -r1;
+  int y = 0;
+  int e2 = r2;
+  int dx = (1 + 2 * x) * e2 * e2;
+  int dy = x * x;
+  int err = dx + dy;
+
+  do {
+    DrawBlock(x1 - x, 2 * (y1 + y), true, s);
+    DrawBlock(x1 + x, 2 * (y1 + y), true, s);
+    DrawBlock(x1 + x, 2 * (y1 - y), true, s);
+    DrawBlock(x1 - x, 2 * (y1 - y), true, s);
+    e2 = 2 * err;
+    if (e2 >= dx) {
+      x++;
+      err += dx += 2 * r2 * r2;
+    }
+    if (e2 <= dy) {
+      y++;
+      err += dy += 2 * r1 * r1;
+    }
+  } while (x <= 0);
+
+  while (y++ < r2) {
+    DrawBlock(x1, 2 * (y1 + y), true, s);
+    DrawBlock(x1, 2 * (y1 - y), true, s);
+  }
+}
+
+void Canvas::DrawBlockEllipseFilled(int x1, int y1, int r1, int r2) {
+  DrawBlockEllipseFilled(x1, y1, r1, r2, [](Pixel&) {});
+}
+
+void Canvas::DrawBlockEllipseFilled(int x1,
+                                     int y1,
+                                     int r1,
+                                     int r2,
+                                     const Color& color) {
+  DrawBlockEllipseFilled(x1, y1, r1, r2,
+                          [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawBlockEllipseFilled(int x1,
+                                     int y1,
+                                     int r1,
+                                     int r2,
+                                     const Stylizer& s) {
+  y1 /= 2;
+  r2 /= 2;
+  int x = -r1;
+  int y = 0;
+  int e2 = r2;
+  int dx = (1 + 2 * x) * e2 * e2;
+  int dy = x * x;
+  int err = dx + dy;
+
+  do {
+    for(int xx = x1+x; xx <= x1-x; ++xx) {
+      DrawBlock(xx, 2 * (y1 + y), true, s);
+      DrawBlock(xx, 2 * (y1 - y), true, s);
+    }
+    e2 = 2 * err;
+    if (e2 >= dx) {
+      x++;
+      err += dx += 2 * r2 * r2;
+    }
+    if (e2 <= dy) {
+      y++;
+      err += dy += 2 * r1 * r1;
+    }
+  } while (x <= 0);
+
+  while (y++ < r2) {
+    for(int yy = y1+y; yy <= y1-y; ++yy) {
+      DrawBlock(x1, 2 * yy, true, s);
+    }
+  }
+}
+
+void Canvas::DrawText(int x, int y, const std::string& value) {
+  DrawText(x, y, value, [](Pixel&) {});
+}
+
+void Canvas::DrawText(int x,
+                      int y,
+                      const std::string& value,
+                      const Color& color) {
+  DrawText(x, y, value, [color](Pixel& p) { p.foreground_color = color; });
+}
+
+void Canvas::DrawText(int x,
+                      int y,
+                      const std::string& value,
+                      const Stylizer& style) {
+  x /= 2;
+  y /= 4;
+  for (const auto& it : Utf8ToGlyphs(value)) {
+    if (!IsIn(x, y))
+      continue;
+    Cell& cell = storage_[XY{x, y}];
+    cell.type = CellType::kText;
+    cell.content.character = it;
+    style(cell.content);
+    x++;
+  }
+}
+
+void Canvas::Style(int x, int y, const Stylizer& style) {
+  if (IsIn(x, y))
+    style(storage_[XY{x / 2, y / 4}].content);
+}
+
+Element ElementFrom(ConstRef<Canvas> canvas) {
+  class Impl : public Node {
+   public:
+    Impl(ConstRef<Canvas> canvas) : canvas_(canvas) {
+      requirement_.min_x = (canvas_->width() + 1) / 2;
+      requirement_.min_y = (canvas_->height() + 3) / 4;
+    }
+
+    void Render(Screen& screen) override {
+      int y_max = std::min(requirement_.min_y, box_.y_max - box_.y_min + 1);
+      int x_max = std::min(requirement_.min_x, box_.x_max - box_.x_min + 1);
+      for (int y = 0; y < y_max; ++y) {
+        for (int x = 0; x < x_max; ++x) {
+          screen.PixelAt(box_.x_min + x, box_.y_min + y) =
+              canvas_->GetPixel(x * 2, y * 4);
+        }
+      }
+    }
+
+   private:
+    ConstRef<Canvas> canvas_;
+  };
+  return std::make_shared<Impl>(std::move(canvas));
+}
+
+}  // namespace ftxui
-- 
GitLab