目录
案例实践:自定义ListView部件,实现显示一个文本列表显示
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 - 知乎
用于描述节点,节点下可以存在多个子节点。通过这样的节点嵌套实现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树操作
下面这个函数定义了渲染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();
}
该文件中定义了预先设计好的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用于显示一个字符串信息,它继承自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用于装饰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是一个横向布局管理器,布局管理器本身不需要渲染内容,而是计算其内部子节点的渲染要求,合理分配内部节点的最终渲染区域,最终管理内部子节点的渲染区域大小。
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));
}
#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;
}