Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions libs/ui/FreeInkUI/include/components/lists/list.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ struct ListItem {
// value string is ignored when set. Activation stays row-level via action.
bool toggle = false;
bool toggleChecked = false;
// Optional section heading drawn immediately before this selectable row.
// It shares the row's logical index and interaction value. Kept last so
// existing aggregate initializers remain source-compatible.
const char *sectionHeading = nullptr;
};

struct ListNav;
Expand All @@ -41,7 +45,9 @@ struct ListProps {
// pin tens of KB of ListItems + label strings for rows that are never
// drawn. list() only touches indexes in [topIndex, topIndex + visible],
// so the caller must keep the window covering that range (refresh it after
// viewport changes, before list()). 0 = items is the full array.
// viewport changes, before list()). Set props.nav for a window whose top may
// enter the last fixed-height page; otherwise list() may clamp top below the
// supplied window. 0 = items is the full array.
uint16_t itemsWindowFirst = 0;
// Number of ListItems supplied in `items` when it is a virtual window.
// Set this whenever itemsWindowFirst is non-zero (or the supplied array is
Expand Down Expand Up @@ -235,6 +241,8 @@ struct ListNav {
}

// Pull the viewport the minimal amount so the selection is visible.
// selected and top must both use absolute row indexes; callers that keep a
// focus sentinel in selected must translate before calling follow().
void follow(const int count) {
followPending = true; // confirmed (or corrected) by onListRendered()
const uint16_t rows =
Expand Down Expand Up @@ -453,8 +461,26 @@ void list(Frame<MaxInteractions> &frame, Rect rect, const ListProps &props) {
} else if (labelLines > 1) {
itemH = static_cast<int16_t>(rowH + labelLh * (labelLines - 1));
}
if (static_cast<int16_t>(cursorY + itemH) > rowArea.bottom())
const bool hasSectionHeading = item.sectionHeading != nullptr && item.sectionHeading[0] != '\0';
const int16_t sectionPad = hasSectionHeading && i != top ? props.sectionGap : 0;
const int16_t sectionH =
hasSectionHeading ? static_cast<int16_t>(sectionPad + headerH + rowGap) : 0;
if (static_cast<int16_t>(cursorY + sectionH + itemH) > rowArea.bottom())
break;
if (hasSectionHeading) {
cursorY = static_cast<int16_t>(cursorY + sectionPad);
Rect headerRow{static_cast<int16_t>(rowArea.x + sidePad), cursorY,
static_cast<int16_t>(rowArea.width - sidePad * 2),
headerLh};
frame.target().text(headerRow, item.sectionHeading, props.headerText);
if (props.headerUnderline) {
frame.target().fill(Rect{headerRow.x,
static_cast<int16_t>(cursorY + headerLh + 2),
headerRow.width, 1},
Paint::solid(props.headerText.color));
}
cursorY = static_cast<int16_t>(cursorY + headerH + rowGap);
}
++drawnRows;
++consumedIndexes;
if (props.selectedIndex == static_cast<int16_t>(i))
Expand Down
65 changes: 65 additions & 0 deletions libs/ui/FreeInkUI/test/host/test_freeinkui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,69 @@ void testListItemsWindowSkipsUnavailablePartialPreview() {
CHECK(!draw.drewForbiddenLabel);
}

void testListInlineSectionHeadingWindow() {
FakeDrawTarget draw;
DeviceContext device = makeDevice();
InputSnapshot input;
InteractionBuffer<16> interactions;
Frame<16> frame(draw, device, input, interactions);

ListItem window[3]{};
window[0].label = "Book A";
window[0].sectionHeading = "Author A";
window[0].actionValue = 10;
window[1].label = "Book B";
window[1].actionValue = 11;
window[2].label = "Book C";
window[2].sectionHeading = "Author C";
window[2].actionValue = 12;

ListNav nav;
ListProps props;
props.items = window;
props.itemsWindowFirst = 10;
props.itemsWindowCount = 3;
props.count = 100;
props.topIndex = 10;
props.action = 9;
props.rowHeight = 40;
props.headerUnderline = false;
props.nav = &nav;
list(frame, Rect{0, 0, 480, 96}, props); // 16px heading + two 40px rows

CHECK_EQ(interactions.count(), 2u);
CHECK_EQ(interactions.data()[0].value, 10);
CHECK_EQ(interactions.data()[1].value, 11);
CHECK_EQ(nav.drawnRows, 2);
CHECK_EQ(draw.countKind(FakeDrawTarget::Op::Text), 3u); // heading + two books
}

void testListInlineSectionHeadingDoesNotOrphan() {
FakeDrawTarget draw;
DeviceContext device = makeDevice();
InputSnapshot input;
InteractionBuffer<4> interactions;
Frame<4> frame(draw, device, input, interactions);

ListItem item{};
item.label = "Book";
item.sectionHeading = "Author";
item.actionValue = 4;

ListNav nav;
ListProps props;
props.items = &item;
props.count = 1;
props.action = 9;
props.rowHeight = 40;
props.nav = &nav;
list(frame, Rect{0, 0, 480, 55}, props); // heading + row need 56px

CHECK_EQ(interactions.count(), 0u);
CHECK_EQ(nav.drawnRows, 0);
CHECK_EQ(draw.countKind(FakeDrawTarget::Op::Text), 0u);
}

void testListNavLayoutFeedback() {
FakeDrawTarget draw;
DeviceContext device = makeDevice();
Expand Down Expand Up @@ -3406,6 +3469,8 @@ int main() {
testListItemsWindow();
testListItemsWindowStopsBeforePastEndMeasurement();
testListItemsWindowSkipsUnavailablePartialPreview();
testListInlineSectionHeadingWindow();
testListInlineSectionHeadingDoesNotOrphan();
testListNavLayoutFeedback();
testListNavConvergesThroughRealList();
testListCanUseFullTitleWidthWithShortValue();
Expand Down