diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index deb5140..d6cf35f 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -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& tabs) { +int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector& 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(i); + break; + } + } + } + // Calculate total width of all tabs and individual tab widths + std::vector 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(tabs.size() - 1) * tabPadding; // Add padding between tabs - // Draw tab label - renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true, - tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); + // 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]; - // Draw underline for selected tab - if (tab.selected) { - renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight); + // 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; } diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 0499448..7e46926 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -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& 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& 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")