feat: Enhanced tab bar with scrolling, overflow indicators, and cursor

Tab bar now scrolls to keep selected tab visible when content overflows.
Adds triangle overflow indicators and optional bullet cursor indicators
around the active tab for visual focus feedback.
This commit is contained in:
cottongin 2026-01-28 02:20:38 -05:00
parent 245d5a7dd8
commit 69a26ccb0e
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
2 changed files with 135 additions and 11 deletions

View File

@ -90,33 +90,155 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
}
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs) {
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs, int selectedIndex, bool showCursor) {
constexpr int tabPadding = 20; // Horizontal padding between tabs
constexpr int leftMargin = 20; // Left margin for first tab
constexpr int rightMargin = 20; // Right margin
constexpr int underlineHeight = 2; // Height of selection underline
constexpr int underlineGap = 4; // Gap between text and underline
constexpr int cursorPadding = 4; // Space between bullet cursor and tab text
constexpr int overflowIndicatorWidth = 16; // Space reserved for < > indicators
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
const int tabBarHeight = lineHeight + underlineGap + underlineHeight;
const int screenWidth = renderer.getScreenWidth();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int availableWidth = screenWidth - bezelLeft - bezelRight - leftMargin - rightMargin;
int currentX = leftMargin;
// Find selected index if not provided
if (selectedIndex < 0) {
for (size_t i = 0; i < tabs.size(); i++) {
if (tabs[i].selected) {
selectedIndex = static_cast<int>(i);
break;
}
}
}
// Calculate total width of all tabs and individual tab widths
std::vector<int> tabWidths;
int totalWidth = 0;
for (const auto& tab : tabs) {
const int textWidth =
renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
tabWidths.push_back(textWidth);
totalWidth += textWidth;
}
totalWidth += static_cast<int>(tabs.size() - 1) * tabPadding; // Add padding between tabs
// Calculate scroll offset to keep selected tab visible
int scrollOffset = 0;
if (totalWidth > availableWidth && selectedIndex >= 0) {
// Calculate position of selected tab
int selectedStart = 0;
for (int i = 0; i < selectedIndex; i++) {
selectedStart += tabWidths[i] + tabPadding;
}
int selectedEnd = selectedStart + tabWidths[selectedIndex];
// If selected tab would be cut off on the right, scroll left
if (selectedEnd > availableWidth) {
scrollOffset = selectedEnd - availableWidth + tabPadding;
}
// If selected tab would be cut off on the left (after scrolling), adjust
if (selectedStart - scrollOffset < 0) {
scrollOffset = selectedStart;
}
}
int currentX = leftMargin + bezelLeft - scrollOffset;
// Bullet cursor settings
constexpr int bulletRadius = 3;
const int bulletCenterY = y + lineHeight / 2;
// Calculate visible area boundaries (leave room for overflow indicators)
const bool hasLeftOverflow = scrollOffset > 0;
const bool hasRightOverflow = totalWidth > availableWidth && scrollOffset < totalWidth - availableWidth;
const int visibleLeft = bezelLeft + (hasLeftOverflow ? overflowIndicatorWidth : 0);
const int visibleRight = screenWidth - bezelRight - (hasRightOverflow ? overflowIndicatorWidth : 0);
for (size_t i = 0; i < tabs.size(); i++) {
const auto& tab = tabs[i];
const int textWidth = tabWidths[i];
// Only draw if at least partially visible (accounting for overflow indicator space)
if (currentX + textWidth > visibleLeft && currentX < visibleRight) {
// Draw bullet cursor before selected tab when showCursor is true
if (showCursor && tab.selected) {
// Draw filled circle using distance-squared check
const int bulletCenterX = currentX - cursorPadding - bulletRadius;
const int radiusSq = bulletRadius * bulletRadius;
for (int dy = -bulletRadius; dy <= bulletRadius; ++dy) {
for (int dx = -bulletRadius; dx <= bulletRadius; ++dx) {
if (dx * dx + dy * dy <= radiusSq) {
renderer.drawPixel(bulletCenterX + dx, bulletCenterY + dy, true);
}
}
}
}
// Draw tab label
renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true,
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
// Draw bullet cursor after selected tab when showCursor is true
if (showCursor && tab.selected) {
// Draw filled circle using distance-squared check
const int bulletCenterX = currentX + textWidth + cursorPadding + bulletRadius;
const int radiusSq = bulletRadius * bulletRadius;
for (int dy = -bulletRadius; dy <= bulletRadius; ++dy) {
for (int dx = -bulletRadius; dx <= bulletRadius; ++dx) {
if (dx * dx + dy * dy <= radiusSq) {
renderer.drawPixel(bulletCenterX + dx, bulletCenterY + dy, true);
}
}
}
}
// Draw underline for selected tab
if (tab.selected) {
renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight);
}
}
currentX += textWidth + tabPadding;
}
// Draw overflow indicators if content extends beyond visible area
if (totalWidth > availableWidth) {
constexpr int triangleHeight = 12; // Height of the triangle (vertical)
constexpr int triangleWidth = 6; // Width of the triangle (horizontal) - thin/elongated
const int triangleCenterY = y + lineHeight / 2;
// Left overflow indicator (more content to the left) - thin triangle pointing left
if (scrollOffset > 0) {
// Clear background behind indicator to hide any overlapping text
renderer.fillRect(bezelLeft, y - 2, overflowIndicatorWidth, lineHeight + 4, false);
// Draw left-pointing triangle: point on left, base on right
const int tipX = bezelLeft + 2;
for (int i = 0; i < triangleWidth; ++i) {
// Scale height based on position (0 at tip, full height at base)
const int lineHalfHeight = (triangleHeight * i) / (triangleWidth * 2);
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight,
tipX + i, triangleCenterY + lineHalfHeight);
}
}
// Right overflow indicator (more content to the right) - thin triangle pointing right
if (scrollOffset < totalWidth - availableWidth) {
// Clear background behind indicator to hide any overlapping text
renderer.fillRect(screenWidth - bezelRight - overflowIndicatorWidth, y - 2, overflowIndicatorWidth, lineHeight + 4, false);
// Draw right-pointing triangle: base on left, point on right
const int baseX = screenWidth - bezelRight - 2 - triangleWidth;
for (int i = 0; i < triangleWidth; ++i) {
// Scale height based on position (full height at base, 0 at tip)
const int lineHalfHeight = (triangleHeight * (triangleWidth - 1 - i)) / (triangleWidth * 2);
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight,
baseX + i, triangleCenterY + lineHalfHeight);
}
}
}
return tabBarHeight;
}

View File

@ -23,7 +23,9 @@ class ScreenComponents {
// Draw a horizontal tab bar with underline indicator for selected tab
// Returns the height of the tab bar (for positioning content below)
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs);
// When selectedIndex is provided, tabs scroll so the selected tab is visible
// When showCursor is true, bullet indicators are drawn around the selected tab
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs, int selectedIndex = -1, bool showCursor = false);
// Draw a scroll/page indicator on the right side of the screen
// Shows up/down arrows and current page fraction (e.g., "1/3")