Split-screen layout: vertical controls left, full-height artwork right

Redesign the 640x480 UI from horizontal bottom-bar controls to a split-screen
layout that maximizes album art visibility for the child user. Buttons stack
vertically in a 200px left panel; artwork fills the remaining right panel at
up to 420x460. Volume removed from focus cycle in favor of dedicated keys
(+/-, volume buttons, shoulder buttons). Navigation changed to UP/DOWN.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Smith 2026-02-14 11:45:32 +01:00
parent 1ea1490e60
commit 201a8fae97
2 changed files with 57 additions and 51 deletions

View File

@ -7,7 +7,7 @@
| Version | 1.0 | | Version | 1.0 |
| Status | Draft | | Status | Draft |
| Created | 2026-02-10 | | Created | 2026-02-10 |
| Updated | 2026-02-13 | | Updated | 2026-02-14 |
## 1. Purpose ## 1. Purpose
@ -43,6 +43,16 @@ This document specifies the functional requirements for an SDL2 based media play
## 6. Changelog ## 6. Changelog
### 2026-02-14 — Split-screen layout with artwork focus
- **Vertical left panel**: Transport controls (Prev, Rewind, Play/Stop, FF, Next) are stacked vertically in a 200px-wide left panel. Play/Stop is slightly larger (72x72) at the vertical center; other buttons are 56x56.
- **Full-height artwork**: Album art now fills the right panel (420x460 max bounds, centered at x=420, y=240), giving cassette covers nearly the full screen height instead of being constrained to the upper portion.
- **Vertical navigation**: D-pad UP/DOWN (and keyboard arrows) now navigate between buttons vertically instead of LEFT/RIGHT horizontally, matching the new stacked layout.
- **Dedicated volume controls**: Volume is no longer a focusable UI element. Adjusted via `+`/`-` keys, `SDLK_VOLUMEUP`/`SDLK_VOLUMEDOWN`, or controller shoulder buttons (L1/R1).
- **Sprite scaling quality**: Linear filtering (`SDL_HINT_RENDER_SCALE_QUALITY`) enabled for smoother downscaling of 200x200 sprite source to 56x56 button destinations.
- **No-art placeholder**: When a file has no embedded album art, a circle sprite from the spritesheet is rendered as a placeholder in the right panel.
- **Thin progress bar**: Progress bar moved to the bottom of the left panel (160x4px) as a subtle position indicator.
### 2026-02-13 — Fix power button screen toggle regression ### 2026-02-13 — Fix power button screen toggle regression
- **Power button screen off stays off**: Fixed regression from fbd32d7 where short-pressing the power button to turn off the screen would instantly turn it back on. The generic wake logic (`any_activity`) was being triggered by power button events themselves. Moved `any_activity = True` below the power button handler's `continue` so power events are handled exclusively by the power button handler and don't trigger the wake path. - **Power button screen off stays off**: Fixed regression from fbd32d7 where short-pressing the power button to turn off the screen would instantly turn it back on. The generic wake logic (`any_activity`) was being triggered by power button events themselves. Moved `any_activity = True` below the power button handler's `continue` so power events are handled exclusively by the power button handler and don't trigger the wake path.

View File

@ -66,13 +66,12 @@ static int current_file_index = 0;
/* --- Focus / navigation --- */ /* --- Focus / navigation --- */
#define FOCUS_VOLUME 0 #define FOCUS_PREV 0
#define FOCUS_PREV 1 #define FOCUS_REWIND 1
#define FOCUS_REWIND 2 #define FOCUS_PLAYSTOP 2
#define FOCUS_PLAYSTOP 3 #define FOCUS_FF 3
#define FOCUS_FF 4 #define FOCUS_NEXT 4
#define FOCUS_NEXT 5 #define FOCUS_COUNT 5
#define FOCUS_COUNT 6
static float volume = 0.5f; static float volume = 0.5f;
static int focus_index = FOCUS_PLAYSTOP; static int focus_index = FOCUS_PLAYSTOP;
@ -596,30 +595,28 @@ int main(int argc, char** argv) {
audio_dir[sizeof(audio_dir) - 1] = '\0'; audio_dir[sizeof(audio_dir) - 1] = '\0';
} }
/* Volume slider (left of buttons) */ /* Left panel: buttons stacked vertically, centered at x=100 */
const SDL_Rect volume_bg = {25, 390, 30, 80}; const SDL_Rect prev_btn = {72, 34, 56, 56};
const SDL_Rect rewind_btn = {72, 120, 56, 56};
/* Button positions (bottom of window, centered) */ const SDL_Rect playstop_btn = {64, 206, 72, 72};
const SDL_Rect prev_btn = {80, 390, 80, 80}; const SDL_Rect ff_btn = {72, 308, 56, 56};
const SDL_Rect rewind_btn = {180, 390, 80, 80}; const SDL_Rect next_btn = {72, 394, 56, 56};
const SDL_Rect playstop_btn = {280, 390, 80, 80};
const SDL_Rect ff_btn = {380, 390, 80, 80};
const SDL_Rect next_btn = {480, 390, 80, 80};
/* Array of focusable rects indexed by FOCUS_* constants */ /* Array of focusable rects indexed by FOCUS_* constants */
const SDL_Rect* focus_rects[FOCUS_COUNT] = {&volume_bg, &prev_btn, &rewind_btn, const SDL_Rect* focus_rects[FOCUS_COUNT] = {&prev_btn, &rewind_btn, &playstop_btn, &ff_btn,
&playstop_btn, &ff_btn, &next_btn}; &next_btn};
/* Sprite sheet source rects */ /* Sprite sheet source rects */
const SDL_Rect rewind_sprite = {0, 0, 200, 200}; const SDL_Rect rewind_sprite = {0, 0, 200, 200};
const SDL_Rect play_sprite = {220, 0, 200, 200}; const SDL_Rect play_sprite = {220, 0, 200, 200};
const SDL_Rect ff_sprite = {440, 0, 200, 200}; const SDL_Rect ff_sprite = {440, 0, 200, 200};
const SDL_Rect stop_sprite = {0, 220, 200, 200}; const SDL_Rect stop_sprite = {0, 220, 200, 200};
const SDL_Rect prev_sprite = {440, 220, 200, 200}; /* same circle as next */ const SDL_Rect prev_sprite = {440, 220, 200, 200};
const SDL_Rect next_sprite = {440, 220, 200, 200}; const SDL_Rect next_sprite = {440, 220, 200, 200};
const SDL_Rect circle_sprite = {440, 220, 200, 200}; /* placeholder for no-art */
/* Progress bar area */ /* Progress bar — thin bar at bottom of left panel */
const SDL_Rect progress_bg = {20, 360, 600, 15}; const SDL_Rect progress_bg = {20, 466, 160, 4};
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 0) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 0) {
panic_and_abort("SDL_Init failed!", SDL_GetError()); panic_and_abort("SDL_Init failed!", SDL_GetError());
@ -683,6 +680,8 @@ int main(int argc, char** argv) {
} }
} }
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
SDL_Surface* controls_surface = IMG_Load("controls.png"); SDL_Surface* controls_surface = IMG_Load("controls.png");
if (!controls_surface) { if (!controls_surface) {
SDL_RWops* rw = SDL_RWFromConstMem(controls_png_data, controls_png_size); SDL_RWops* rw = SDL_RWFromConstMem(controls_png_data, controls_png_size);
@ -788,17 +787,20 @@ int main(int argc, char** argv) {
case SDL_KEYDOWN: case SDL_KEYDOWN:
switch (e.key.keysym.sym) { switch (e.key.keysym.sym) {
case SDLK_LEFT: case SDLK_UP:
focus_index = (focus_index - 1 + FOCUS_COUNT) % FOCUS_COUNT; focus_index = (focus_index - 1 + FOCUS_COUNT) % FOCUS_COUNT;
break; break;
case SDLK_RIGHT: case SDLK_DOWN:
focus_index = (focus_index + 1) % FOCUS_COUNT; focus_index = (focus_index + 1) % FOCUS_COUNT;
break; break;
case SDLK_UP: case SDLK_EQUALS:
if (focus_index == FOCUS_VOLUME) adjust_volume(0.05f); case SDLK_PLUS:
case SDLK_VOLUMEUP:
adjust_volume(0.05f);
break; break;
case SDLK_DOWN: case SDLK_MINUS:
if (focus_index == FOCUS_VOLUME) adjust_volume(-0.05f); case SDLK_VOLUMEDOWN:
adjust_volume(-0.05f);
break; break;
case SDLK_RETURN: case SDLK_RETURN:
case SDLK_KP_ENTER: case SDLK_KP_ENTER:
@ -809,17 +811,17 @@ int main(int argc, char** argv) {
case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONDOWN:
switch (e.cbutton.button) { switch (e.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_LEFT: case SDL_CONTROLLER_BUTTON_DPAD_UP:
focus_index = (focus_index - 1 + FOCUS_COUNT) % FOCUS_COUNT; focus_index = (focus_index - 1 + FOCUS_COUNT) % FOCUS_COUNT;
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
focus_index = (focus_index + 1) % FOCUS_COUNT; focus_index = (focus_index + 1) % FOCUS_COUNT;
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_UP: case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
if (focus_index == FOCUS_VOLUME) adjust_volume(0.05f); adjust_volume(-0.05f);
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN: case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
if (focus_index == FOCUS_VOLUME) adjust_volume(-0.05f); adjust_volume(0.05f);
break; break;
case SDL_CONTROLLER_BUTTON_A: case SDL_CONTROLLER_BUTTON_A:
activate_focused_button(); activate_focused_button();
@ -829,14 +831,10 @@ int main(int argc, char** argv) {
case SDL_JOYHATMOTION: { case SDL_JOYHATMOTION: {
Uint8 hat = e.jhat.value; Uint8 hat = e.jhat.value;
if (hat & SDL_HAT_LEFT) { if (hat & SDL_HAT_UP) {
focus_index = (focus_index - 1 + FOCUS_COUNT) % FOCUS_COUNT; focus_index = (focus_index - 1 + FOCUS_COUNT) % FOCUS_COUNT;
} else if (hat & SDL_HAT_RIGHT) {
focus_index = (focus_index + 1) % FOCUS_COUNT;
} else if (hat & SDL_HAT_UP) {
if (focus_index == FOCUS_VOLUME) adjust_volume(0.05f);
} else if (hat & SDL_HAT_DOWN) { } else if (hat & SDL_HAT_DOWN) {
if (focus_index == FOCUS_VOLUME) adjust_volume(-0.05f); focus_index = (focus_index + 1) % FOCUS_COUNT;
} }
break; break;
} }
@ -919,16 +917,20 @@ int main(int argc, char** argv) {
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF); SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
SDL_RenderClear(renderer); SDL_RenderClear(renderer);
/* Album art (centered, aspect-preserving, in upper area) */ /* Album art (right panel, centered, aspect-preserving) */
if (decoder.album_art && decoder.art_width > 0 && decoder.art_height > 0) { if (decoder.album_art && decoder.art_width > 0 && decoder.art_height > 0) {
int max_w = 600, max_h = 340; int max_w = 420, max_h = 460;
float scale_w = (float)max_w / decoder.art_width; float scale_w = (float)max_w / decoder.art_width;
float scale_h = (float)max_h / decoder.art_height; float scale_h = (float)max_h / decoder.art_height;
float scale = scale_w < scale_h ? scale_w : scale_h; float scale = scale_w < scale_h ? scale_w : scale_h;
int draw_w = (int)(decoder.art_width * scale); int draw_w = (int)(decoder.art_width * scale);
int draw_h = (int)(decoder.art_height * scale); int draw_h = (int)(decoder.art_height * scale);
SDL_Rect art_rect = {(640 - draw_w) / 2, (350 - draw_h) / 2, draw_w, draw_h}; SDL_Rect art_rect = {420 - draw_w / 2, 240 - draw_h / 2, draw_w, draw_h};
SDL_RenderCopy(renderer, decoder.album_art, NULL, &art_rect); SDL_RenderCopy(renderer, decoder.album_art, NULL, &art_rect);
} else {
/* No-art placeholder: circle sprite scaled up in right panel */
SDL_Rect placeholder = {420 - 100, 240 - 100, 200, 200};
SDL_RenderCopy(renderer, controls_texture, &circle_sprite, &placeholder);
} }
/* Progress bar */ /* Progress bar */
@ -946,15 +948,9 @@ int main(int argc, char** argv) {
SDL_RenderFillRect(renderer, &fill); SDL_RenderFillRect(renderer, &fill);
} }
/* Volume slider */ /* Separator line between left panel and right panel */
SDL_SetRenderDrawColor(renderer, 0xC0, 0xC0, 0xC0, 0xFF); SDL_SetRenderDrawColor(renderer, 0xC0, 0xC0, 0xC0, 0xFF);
SDL_RenderFillRect(renderer, &volume_bg); SDL_RenderDrawLine(renderer, 200, 10, 200, 470);
{
int fill_h = (int)(volume_bg.h * volume);
SDL_Rect vol_fill = {volume_bg.x, volume_bg.y + volume_bg.h - fill_h, volume_bg.w, fill_h};
SDL_SetRenderDrawColor(renderer, 0x50, 0x50, 0x50, 0xFF);
SDL_RenderFillRect(renderer, &vol_fill);
}
/* Buttons */ /* Buttons */
SDL_RenderCopy(renderer, controls_texture, &prev_sprite, &prev_btn); SDL_RenderCopy(renderer, controls_texture, &prev_sprite, &prev_btn);