Widgets and Focus

A Widget is a drawable object inside an App, such as a text button, number selector, or line chart. Apps and Widgets belong to different layers: ViewManager manages Apps, while Widgets are usually App members and share their App’s lifetime.

An interactive button

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 belongs to PixelUI. ui.handleInput() first lets FocusManager process LEFT, RIGHT, and SELECT, then passes unconsumed events to the current App.

Focusable state

IWidget defaults to focusable = false. TextButton, IconButton, Label, and NumScroll make themselves focusable; Clock is display-only. Explicitly enable focus when a Brace, Histogram, or CurveChart must participate in navigation:

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

FocusManager navigates only nodes that are focusable, visible, and enabled, and whose ancestors are also visible and enabled.

Widget parent-child tree

The Widget tree uses intrusive parent-child links between caller-owned Widget objects, with no separate node allocation:

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();

// Register only the root. FocusManager traverses children with DFS.
ui_.addWidgetToFocusManager(&panel_);
}

void draw() override {
// Drawing the root recursively draws its children.
panel_.draw();
}

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

Child coordinates are local to the parent. Children move with the parent. By default, a parent’s bounds clip its children; call setClipChildren(false) to disable clipping.

Widgets are neither copyable nor movable. When a Widget is destroyed or detached from the tree, FocusManager is notified and clears current or active pointers to it.

Built-in Widget reference

Widget Purpose Main configuration
TextButton Text button setCallback, setText, font and text offsets
IconButton XBM icon button setSource, setCallback
Label Text with directional entrance Animation setText, setLoadPos, setCallback
NumScroll Integer selection setRange, setFormatter, setValue, getValue
Clock Analog clock setHour, setMinute, setSecond, setRadius
Brace Border or container setDrawContentFunction, addChild, setCallback
ProgressBar Percentage progress bar setPercent, setPercentImmediate, getPercent
Histogram Bar history chart StaticChartSeries::add, window and history statistics
CurveChart Line history chart StaticChartSeries::add, window and history statistics
BitmapWidget Bitmap display Configure the bitmap and display area

Callbacks use PixelUI’s fixed-capacity VoidCallback. Stored const char* strings and XBM pointers are non-owning and require backing storage that covers the Widget lifetime.

Widgets that support LoadTransition play their entrance Animation by default. To start directly in the final state, call widget.onLoad(LoadTransition::Immediate). Older names such as TextButton::onLoadNoAnim() and Label::onLoadImmediately() remain as compatibility interfaces, but new code should prefer LoadTransition. Not every Widget provides this overload; ProgressBar, for example, selects Animation behavior through its useAnimation constructor argument.

NumScroll::setValueImmediate() displays a new value synchronously while retaining the fixed-capacity and UI-task constraints.

TextButton and Label

TextButton Label
TextButton geometry Label slide-in direction

TextButton uses (x, y, width, height) for its rectangle. Label’s POS selects the direction of its entrance Animation.

TextButton can receive a custom font in its constructor, followed by signed textOffsetX and textOffsetY values. When using the default font, pass the two offsets directly after the text argument:

1
2
3
4
5
6
7
TextButton compactButton{
ui_, 34, 24, 60, 16,
"Select", u8g2_font_5x7_mf,
0, -1
};

TextButton defaultFontButton{ui_, 34, 44, 60, 16, "Back", 0, -1};

The offsets slightly adjust the final text position. setText(text, offsetX, offsetY) replaces the text and offsets together.

ProgressBar

The built-in ProgressBar is a non-focusable, display-only Widget. Its percentage is always clamped to 0..100:

1
2
3
4
5
6
7
ProgressBar animated_{ui_, 14, 22, 100, 8, 25};
ProgressBar immediate_{ui_, 14, 38, 100, 8, 25, false};

void updateProgress(int32_t percent) {
animated_.setPercent(percent);
immediate_.setPercentImmediate(percent);
}

The useAnimation constructor argument defaults to true. In that mode, onLoad() animates from zero to the current target and setPercent() animates from the displayed value to the new value. Passing false makes both operations immediate. Regardless of that setting, setPercentImmediate() cancels this progress bar’s own Animation and displays the new value immediately. If the Animation pool is full, the Widget displays the target value directly instead of remaining at an intermediate value.

NumScroll and Clock

NumScroll Clock
NumScroll geometry Analog clock geometry

NumScroll coordinates describe the rectangle’s top-left corner. Clock uses center (x, y) and radius. Clock is display-only by default and does not participate in Focus navigation.

Brace

Brace geometry

Brace can draw custom content with setDrawContentFunction() or serve as a parent in the Widget tree. Diagram coordinates and dimensions describe its local bounds.

Fixed Chart buffers

Histogram and CurveChart use sample storage provided by the caller:

1
2
3
4
5
6
7
8
9
10
StaticChartSeries<76> samples_;

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

StaticChartSeries owns a fixed-capacity int32_t ring buffer and is passed by non-owning reference. Its capacity must equal the expanded width; a template static_assert verifies this relationship. The series must outlive the Widget, so declare it before the Chart member. Add samples with samples_.add(value).

Histogram CurveChart
Histogram expansion geometry CurveChart expansion geometry

Both Charts use the same expansion geometry: EXPAND_BASE selects the fixed corner, and ChartExpandSize<W, H> sets expanded width and height. Histogram draws bars; CurveChart draws a continuous line.