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