Widgets and Focus / 组件与焦点光标

Widget 是 App 内部的可绘制对象,例如文字按钮、数字选择器和曲线图。它和 App 不是同一层:App 由 ViewManager 管理,Widget 通常是 App 的成员,生命周期跟着 App。

一个可操作按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ButtonApp final : public IApplication {
public:
ButtonApp(PixelUI& ui, void*)
: ui_(ui), button_(ui, 34, 24, 60, 16, "Select") {}

void onEnter(ExitCallback cb) override {
IApplication::onEnter(cb);
button_.setCallback([this]() {
selected_ = !selected_;
ui_.markDirty();
});
button_.onLoad();
ui_.addWidgetToFocusManager(&button_);
}

void draw() override {
button_.draw();
}

bool handleInput(InputEvent event) override {
if (event == InputEvent::BACK) {
requestExit();
return true;
}
return false;
}

private:
PixelUI& ui_;
TextButton button_;
bool selected_ = false;
};

FocusManager 属于 PixelUI,不再由每个 App 创建。ui.handleInput() 会先给 FocusManager 处理 LEFT / RIGHT / SELECT,未被消耗的事件再交给当前 App。

可聚焦状态

IWidget 默认 focusable = falseTextButtonIconButtonLabelNumScroll 在自己的实现中已设为可聚焦;Clock 是纯显示组件。BraceHistogramCurveChart 需要参与导航时,请显式设置:

1
2
chart_.setFocusable(true);
ui_.addWidgetToFocusManager(&chart_);

FocusManager 只会导航同时满足以下条件的节点:可聚焦、可见、已启用,且它的所有祖先也可见并已启用。

Widget 父子树

Widget 树不分配节点,也不拥有子对象。它只在已存在的 Widget 之间建立侵入式父子链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class PanelApp final : public IApplication {
public:
PanelApp(PixelUI& ui, void*)
: ui_(ui), panel_(ui, 16, 20, 96, 36),
left_(ui, 7, 10, 38, 16, "Left"),
right_(ui, 51, 10, 38, 16, "Right") {
panel_.addChild(left_);
panel_.addChild(right_);
}

void onEnter(ExitCallback cb) override {
IApplication::onEnter(cb);
panel_.onLoad();
left_.onLoad();
right_.onLoad();

// 只注册根节点,FocusManager 会 DFS 遍历子节点。
ui_.addWidgetToFocusManager(&panel_);
}

void draw() override {
// 画根节点会递归画出子节点。
panel_.draw();
}

private:
PixelUI& ui_;
Brace panel_;
TextButton left_;
TextButton right_;
};

子 Widget 的坐标是相对父 Widget 的局部坐标。父节点移动后,子节点一起移动。默认子节点会被父节点边界裁剪,可通过 setClipChildren(false) 关闭。

Widget 不可拷贝、不可移动。销毁或脱离树时,FocusManager 会收到通知并清理指向它的当前/激活指针。

内置 Widget 速查

Widget 用途 主要配置
TextButton 文字按钮 setCallback, setText, setPosition, setSize
IconButton XBM 图标按钮 setSource, setCallback
Label 文本与方向入场动画 setText, setLoadPos, setCallback
NumScroll 整数选择 setRange, setValue, getValue, setFixedIntDigits
Clock 模拟时钟 setHour, setMinute, setSecond, setRadius
Brace 边框/容器 setDrawContentFunction, addChild, setCallback
Histogram 柱状历史图 addData, 窗口/历史统计
CurveChart 曲线历史图 addData, 窗口/历史统计

回调类型是 PixelUI 定义的 VoidCallback,不是 std::function。组件保存的 const char* 和 XBM 指针也是非拥有的,不要指向已经离开作用域的数组。

TextButton 与 Label

TextButton Label
TextButton geometry Label slide-in direction

TextButton 用 (x, y, width, height) 描述矩形;Label 的 POS 决定入场动画从哪个方向进入。

NumScroll 与 Clock

NumScroll Clock
NumScroll geometry Analog clock geometry

NumScroll 的坐标是矩形左上角;Clock 使用圆心 (x, y) 和半径。Clock 默认是纯显示 Widget,不参与 Focus 导航。

Brace

Brace geometry

Brace 既可以用 setDrawContentFunction() 画自定义内容,也可以作为 Widget 树的父容器。图中的坐标和尺寸对应它自身的局部边界。

Chart 的固定缓冲区

Histogram 和 CurveChart 不会自己在堆上分配样本 buffer,调用者提供存储:

1
2
3
4
5
6
7
8
9
10
float samples_[76]{};

CurveChart chart_{
ui_,
69, 45, 56, 18,
samples_,
ChartExpandSize<76, 63>{},
EXPAND_BASE::BOTTOM_RIGHT,
"Curve"
};

buffer 元素数必须等于 expanded width,这个关系由模板 static_assert 在编译时检查。buffer 必须比 Widget 活得更久;把它声明在 Chart 成员之前,可以保证析构顺序正确。

Histogram CurveChart
Histogram expansion geometry CurveChart expansion geometry

两种 Chart 共用相同的展开几何概念:EXPAND_BASE 决定哪个角保持不动,ChartExpandSize<W, H> 决定展开后的宽高。Histogram 画柱,CurveChart 画连续折线。