当前位置: 首页 > 工具软件 > FTXUI > 使用案例 >

FTXUI 笔记(三)——dom模块

夏朝
2023-12-01

目录

requirement.hpp

node.hpp

Node基类

dom树渲染流程(渲染Node树)

elements.hpp

Text(部件Widget)

Bold(装饰器Decorator)

HBox(布局Layout)

案例实践:自定义ListView部件,实现显示一个文本列表显示


requirement.hpp

Requirement类,定义了计算布局相关的参数。

struct Requirement { // 定义布局相关的参数
  // 当前节点渲染内容所需的最小尺寸
  int min_x = 0;
  int min_y = 0;

  // 当一个父节点的Box边界空间存放所有子节点后仍然存在剩余空间时
  // grow参数会存储所有同级别的节点需要增加渲染区域的比重,也就是占用父节点剩余空间的比重
  int flex_grow_x = 0;
  int flex_grow_y = 0;
  // 当一个父节点的Box边界空间存放所有子节点后发现空间不够时
  // shrink参数会存储所有同级别的节点需要收缩渲染区域的比重
  int flex_shrink_x = 0;
  int flex_shrink_y = 0;

  // Selection定义了节点状态
  enum Selection {
    NORMAL = 0,   // 正常
    SELECTED = 1, // 选中
    FOCUSED = 2,  // 聚焦
  };
  Selection selection = NORMAL; //存储节点的状态
  Box selected_box; //存储选中的区域尺寸
};

关于flex_grow参数和flex_shrink参数的具体介绍,详解 flex-grow 与 flex-shrink - 知乎

node.hpp

Node基类

用于描述节点,节点下可以存在多个子节点。通过这样的节点嵌套实现dom树。

class Node {
 public:
  Node();
  Node(Elements children);
  Node(const Node&) = delete;
  Node(const Node&&) = delete;
  Node& operator=(const Node&) = delete;
  Node& operator=(const Node&&) = delete;

  virtual ~Node();

  // 计算当前节点下的所有子节点的布局参数和渲染区域参数
  virtual void ComputeRequirement();

  // 返回当前节点的布局参数、渲染区域信息
  Requirement requirement() { return requirement_; }

  // 设置当前节点的显示区域尺寸
  virtual void SetBox(Box box);

  // 将当前节点下所有的子节点内容渲染到Screen中
  virtual void Render(Screen& screen);

  // 根节点通过设置多次迭代,完成所有子节点的计算工作
  struct Status {
    int iteration = 0;
    bool need_iteration = false;
  };
  virtual void Check(Status* status);

 protected:
  Elements children_; //存储当前节点的所有子节点
  Requirement requirement_; // 存储当前节点的布局信息
  Box box_;// 存储当前节点最终的绘制大小
};

void Render(Screen& screen, const Element& element);//内部调用渲染Node树的操作
void Render(Screen& screen, Node* node);//渲染Node树操作

dom树渲染流程(渲染Node树)

下面这个函数定义了渲染Node树到Screen绘制区域所需的流程。

// 实现渲染Node树到Screen绘制区域的具体操作
void Render(Screen& screen, Node* node) {

  //获取Screen显示区域的尺寸
  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树是否布局计算完成
  node->Check(&status);// 将状态传递给Node树的所有节点

  const int max_iterations = 20;//定义布局计算的最大次数
  while (status.need_iteration && status.iteration < max_iterations) {
    // 递归调用Node树,完成布局参数和渲染区域的计算操作
    node->ComputeRequirement();

    // 设置根节点的显示区域大小
    node->SetBox(box);

    // 设置根节点已完成布局计算
    status.need_iteration = false;
    status.iteration++;
    
    // 递归调用Node树,完成所有节点的的检查工作
    node->Check(&status);
  }

  // 递归调用Node树,完成所有节点的渲染操作
  screen.stencil = box;
  node->Render(screen);

  // 完成Screen绘制区域内多个Pixel显示单元的字符合并、显示优化操作
  screen.ApplyShader();
}

elements.hpp

该文件中定义了预先设计好的dom树节点类型,我们可以通过预先设计好的element类型的各种组合,实现我们想要的界面。

class Node;
using Element = std::shared_ptr<Node>;
using Elements = std::vector<Element>;
using Decorator = std::function<Element(Element)>;
using GraphFunction = std::function<std::vector<int>(int, int)>;

enum BorderStyle { LIGHT, HEAVY, DOUBLE, ROUNDED, EMPTY };
enum class GaugeDirection { Left, Up, Right, Down };

以上是elements.hpp的源码。

在创建一个element元素时,都是使用对应的函数来创建对象的,然后返回一个基类指针类型。例如下面这个,创建对象使用text函数创建,然后返回一个Element类型。

// 在elements.hpp文件中声明
Element text(std::string text);

// 在text.cpp文件中定义
class Text : public Node {
 public:
  explicit Text(std::string text) : text_(std::move(text)) {}
  ...
};
Element text(std::string text) {
  return std::make_shared<Text>(std::move(text));
}

其次是布局管理器,布局管理器的构造函数一般是通过接收一个Elements类型,然后布局管理器自身最为一个Element。例如下面这个:

// 在elements.hpp文件中声明
Element hbox(Elements);

// 在hbox.cpp文件中定义
class HBox : public Node {
 public:
  explicit HBox(Elements children) : Node(std::move(children)) {}
 ...
};

Element hbox(Elements children) {
  return std::make_shared<HBox>(std::move(children));
}

然后是装饰器,装饰器与普通的Element类型一样,但是通过重载方法实现一些特殊的用法:

// 在elements.hpp中定义
Element operator|(Element, Decorator);
Element& operator|=(Element&, Decorator);
Elements operator|(Elements, Decorator);
Decorator operator|(Decorator, Decorator);

// 实现Element元素与Decorator装饰器组合使用的方式,例如: text() | bold
Element operator|(Element element, Decorator decorator) {  // NOLINT
  return decorator(std::move(element));
}
// 语法同上边一样
Element& operator|=(Element& e, Decorator d) {
  e = e | std::move(d);
  return e;
}
// 实现Elements容器与Decorator装饰器组合使用的方式,例如: hbox() | bold
Elements operator|(Elements elements, Decorator decorator) {  // NOLINT
  Elements output;
  for (auto& it : elements) {
    output.push_back(std::move(it) | decorator);
  }
  return output;
}
// 实现Decorator装饰器与Decorator装饰器组合使用的方式,例如: inverted | bold
Decorator operator|(Decorator a, Decorator b) {
  return compose(std::move(a),  //
                 std::move(b));
}

下面通过分析Text部件、Bold装饰器、HBox布局管理器的源码来深入了解FTXUI库的element元素的设计方法。

Text(部件Widget)

text用于显示一个字符串信息,它继承自Node基类,因此可以挂在Node树下渲染。

class Text : public Node {
 public:
  // 构造函数,初始化Text类要显示的字符串内容
  explicit Text(std::string text) : text_(std::move(text)) {}

  // 重写ComputeRequirement方法,完成布局参数的计算
  void ComputeRequirement() override {
    // Text节点显示内容的最小尺寸参数设置如下
    requirement_.min_x = string_width(text_); // 横向尺寸为字符串的显示宽度
    requirement_.min_y = 1; // 纵向尺寸为1行
  }

  // 重写Render方法,完成Text节点的渲染操作
  void Render(Screen& screen) override {
    // 通过box_获取显示区域的尺寸
    int x = box_.x_min;
    int y = box_.y_min;

    // 若Text渲染区域大于显示区域则直接退出
    if (y > box_.y_max) {
      return;
    }
    for (const auto& cell : Utf8ToGlyphs(text_)) {
      if (x > box_.x_max) {
        return;
      }
      screen.PixelAt(x, y).character = cell;
      ++x;
    }
  }

 private:
  std::string text_;
};

/// @brief Display a piece of UTF8 encoded unicode text.
/// @ingroup dom
/// @see ftxui::to_wstring
///
/// ### Example
///
/// ```cpp
/// Element document = text("Hello world!");
/// ```
///
/// ### Output
///
/// ```bash
/// Hello world!
/// ```
Element text(std::string text) {
  return std::make_shared<Text>(std::move(text));
}

Bold(装饰器Decorator)

bold用于装饰Widget内部的显示,装饰器通过修改Pixel的修饰符实现子节点内Pixel的默认修饰符。

// Helper class.
class NodeDecorator : public Node {
 public:
  NodeDecorator(Element child) : Node(unpack(std::move(child))) {}
  void ComputeRequirement() override;
  void SetBox(Box box) override;
};

void NodeDecorator::ComputeRequirement() {
  Node::ComputeRequirement();
  
  //装饰器本身并不需要渲染内容,所以计算布局时直接将子节点的布局信息保存起来
  requirement_ = children_[0]->requirement();
}

void NodeDecorator::SetBox(Box box) {
  Node::SetBox(box);

  //最终会将box渲染区域直接传递给子节点
  children_[0]->SetBox(box);
}
class Bold : public NodeDecorator {
 public:
  using NodeDecorator::NodeDecorator;

  void Render(Screen& screen) override {
    for (int y = box_.y_min; y <= box_.y_max; ++y) {
      for (int x = box_.x_min; x <= box_.x_max; ++x) {
        screen.PixelAt(x, y).bold = true;
      }
    }
    Node::Render(screen);
  }
};

/// @brief Use a bold font, for elements with more emphasis.
/// @ingroup dom
Element bold(Element child) {
  return std::make_shared<Bold>(std::move(child));
}

HBox(布局Layout)

HBox是一个横向布局管理器,布局管理器本身不需要渲染内容,而是计算其内部子节点的渲染要求,合理分配内部节点的最终渲染区域,最终管理内部子节点的渲染区域大小。

class HBox : public Node {
 public:
  explicit HBox(Elements children) : Node(std::move(children)) {}

  void ComputeRequirement() override {
    requirement_.min_x = 0;
    requirement_.min_y = 0;
    requirement_.flex_grow_x = 0;
    requirement_.flex_grow_y = 0;
    requirement_.flex_shrink_x = 0;
    requirement_.flex_shrink_y = 0;
    requirement_.selection = Requirement::NORMAL;
    
    // 遍历所有子节点,计算它们各自的布局参数
    for (auto& child : children_) {
      child->ComputeRequirement();
      
      // 将子节点中selection最大的取出来,保存在当前的布局管理器中
      // 并同时记录选中的Box区域信息
      if (requirement_.selection < child->requirement().selection) {
        requirement_.selection = child->requirement().selection;
        requirement_.selected_box = child->requirement().selected_box;
        requirement_.selected_box.x_min += requirement_.min_x;
        requirement_.selected_box.x_max += requirement_.min_x;
      }
      
      // 累加所有子节点的横向渲染长度,计算最终的横向长度作为布局管理器的横向长度
      requirement_.min_x += child->requirement().min_x;
      // 取子节点中最大的纵向渲染长度作为布局管理器的渲染长度
      requirement_.min_y =
          std::max(requirement_.min_y, child->requirement().min_y);
    }
  }

  void SetBox(Box box) override {
    Node::SetBox(box);
    
    // 获取所有子节点的布局渲染信息,存到elements中
    std::vector<box_helper::Element> elements(children_.size());
    for (size_t i = 0; i < children_.size(); ++i) {
      auto& element = elements[i];
      const auto& requirement = children_[i]->requirement();
      element.min_size = requirement.min_x;
      element.flex_grow = requirement.flex_grow_x;
      element.flex_shrink = requirement.flex_shrink_x;
    }
    // 计算布局管理器的横向渲染长度
    int target_size = box.x_max - box.x_min + 1;
    // 根据各个子节点的布局渲染信息,计算出它们各自最终应该渲染的Box大小
    box_helper::Compute(&elements, target_size);
    
    // 将它们各自的Box大小传递个相应的子节点
    int x = box.x_min;
    for (size_t i = 0; i < children_.size(); ++i) {
      box.x_min = x;
      box.x_max = x + elements[i].size - 1;
      children_[i]->SetBox(box);
      x = box.x_max + 1;
    }
  }
};

/// @brief A container displaying elements horizontally one by one.
/// @param children The elements in the container
/// @return The container.
///
/// #### Example
///
/// ```cpp
/// hbox({
///   text("Left"),
///   text("Right"),
/// });
/// ```
Element hbox(Elements children) {
  return std::make_shared<HBox>(std::move(children));
}

案例实践:自定义ListView部件,实现显示一个文本列表显示

#include <algorithm>
#include <array>
#include <chrono>
#include <iostream>
#include <random>
#include <string>
#include <vector>

#include "ftxui/dom/elements.hpp"
#include "ftxui/dom/node.hpp"
#include "ftxui/screen/string.hpp"

class ListView : public ftxui::Node {
 public:
  ListView(std::vector<std::string> data) : list(data){};
  void ComputeRequirement() override {
    int x = 0;
    for (auto& item : list) {
      x = std::max(x, ftxui::string_width(item));
    }

    requirement_.min_x = x;
    requirement_.min_y = list.size();
  }

  void Render(ftxui::Screen& screen) override {
    int x = box_.x_min;
    int y = box_.y_min;
    if (x > box_.x_max) {
      return;
    }
    if (y > box_.y_max) {
      return;
    }

    for (auto& item : list) {
      for (const auto& cell : ftxui::Utf8ToGlyphs(item)) {
        screen.PixelAt(x, y).character = cell;
        ++x;
        if (x > box_.x_max) {
          screen.PixelAt(x - 1, y).character = '~';
          break;
        }
      }
      x = 0;
      y++;
      if (y > box_.y_max) {
        break;
      }
    }
  }

 private:
  std::vector<std::string> list;
};

ftxui::Element list_view(std::vector<std::string> data) {
  return std::make_shared<ListView>(data);
}

调用自定义组件,实现屏幕显示

int main(void) {
  using namespace ftxui;

  std::vector<std::string> data;
  data.push_back("Whatever is worth doing is worth doing well.");
  data.push_back("how are you");
  data.push_back("A heart that loves is always young.");

  auto document = list_view(data);
  auto screen = Screen::Create(Dimension::Full());
  Render(screen, document);
  screen.Print();

  return 0;
}

 类似资料: