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:
parent
245d5a7dd8
commit
69a26ccb0e
@ -90,33 +90,155 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si
|
|||||||
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
|
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 tabPadding = 20; // Horizontal padding between tabs
|
||||||
constexpr int leftMargin = 20; // Left margin for first tab
|
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 underlineHeight = 2; // Height of selection underline
|
||||||
constexpr int underlineGap = 4; // Gap between text and 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 lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
||||||
const int tabBarHeight = lineHeight + underlineGap + underlineHeight;
|
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) {
|
for (const auto& tab : tabs) {
|
||||||
const int textWidth =
|
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
||||||
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
|
||||||
|
|
||||||
// Draw tab label
|
// Calculate scroll offset to keep selected tab visible
|
||||||
renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true,
|
int scrollOffset = 0;
|
||||||
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
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 selected tab would be cut off on the right, scroll left
|
||||||
if (tab.selected) {
|
if (selectedEnd > availableWidth) {
|
||||||
renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight);
|
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;
|
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;
|
return tabBarHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,9 @@ class ScreenComponents {
|
|||||||
|
|
||||||
// Draw a horizontal tab bar with underline indicator for selected tab
|
// Draw a horizontal tab bar with underline indicator for selected tab
|
||||||
// Returns the height of the tab bar (for positioning content below)
|
// 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
|
// Draw a scroll/page indicator on the right side of the screen
|
||||||
// Shows up/down arrows and current page fraction (e.g., "1/3")
|
// Shows up/down arrows and current page fraction (e.g., "1/3")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user