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
167 changes: 167 additions & 0 deletions docs/list-views.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# List Views

This document describes the list view system in minui (the launcher).

## Overview

List views are the primary UI element in minui, used for ROM browsing, tools, recents, and collections.

**Implementation:** `workspace/all/minui/minui.c`

## Current Features

### Sorting

ROM lists are sorted using natural sort with No-Intro conventions:

| Feature | Description |
|---------|-------------|
| Natural sort | Numbers compared by value: "Game 2" < "Game 10" |
| Article skipping | "The Legend of Zelda" sorts under "L", not "T" |
| Case insensitive | "mario" and "Mario" sort together |

Supported articles for sort skipping: "The ", "A ", "An " (must have trailing space)

### Navigation

| Feature | Description |
|---------|-------------|
| Single item scroll | D-pad up/down moves one item at a time |
| Page jump | D-pad left/right jumps by visible row count |
| Alphabetic jump | L1/R1 jumps to next/previous letter group (A-Z, #) |
| Wrap-around | Scrolling past bottom wraps to top and vice versa |
| Button repeat | 300ms initial delay, then 100ms repeat interval |

### Selection & Highlighting

| Feature | Description |
|---------|-------------|
| Selection pill | White rounded rectangle behind selected item |
| Text color flip | Selected text changes from white to black |

### Text Display

| Feature | Description |
|---------|-------------|
| Text truncation | Names exceeding available width get ellipsis (...) |
| Display name cleaning | Strips file extensions, region codes, parenthetical metadata |
| Article fixing | "Legend of Zelda, The" displays as "The Legend of Zelda" |
| Sorting prefix removal | Strips "NNN) " prefixes used for custom sort order |
| Duplicate disambiguation | Shows filename or emulator name when display names collide |

### Display Name Aliasing (map.txt)

Custom display names can be defined via `map.txt` files using tab-delimited format:

```
mario.gb Super Mario Land
tetris.gb Tetris DX
unwanted.gb .hidden
```

| Location | Purpose |
|----------|---------|
| `/Roms/GB/map.txt` | Aliases for ROMs in that system folder |
| `/Roms/map.txt` | Aliases for system folder names at root |
| `/Collections/map.txt` | Aliases for collection entries |

**Special behaviors:**
- Alias starting with `.` hides the entry from the list
- List re-sorts alphabetically after aliases are applied
- Aliases are also used by minarch for in-game title display

### Thumbnails

| Feature | Description |
|---------|-------------|
| Thumbnail display | Shows image on right side when ROM is selected |
| Path convention | ROM at `/Roms/GB/game.gb` → thumbnail at `/Roms/GB/.res/game.gb.png` |
| Scaling | Preserves aspect ratio, max 50% screen width |
| Caching | Thumbnail cached to avoid reloading on every frame |

### List Types

| List | Description |
|------|-------------|
| ROM list | Files within a system folder |
| Root directory | System folders, Tools, Recently Played, Collections |
| Recently Played | Last 24 games launched (stored in `.minui/recent.txt`) |
| Collections | Custom ROM lists defined by `.txt` files |
| Multi-disc | Disc selection for games with `.m3u` playlists |

### Hardware Status

| Feature | Description |
|---------|-------------|
| Battery indicator | Shows current charge level (top-right) |
| Brightness indicator | Shows when adjusting brightness |
| Volume indicator | Shows when adjusting volume |

### Button Hints

Context-sensitive button labels displayed at bottom of screen:
- OPEN / SELECT - A button action
- BACK - B button action
- RESUME - X button when save state exists
- Action-specific labels per menu context

### Visual Styling

| Property | Value |
|----------|-------|
| Indentation | Tabs (4-wide) |
| Font sizes | Large (16pt), Medium (14pt), Small (12pt), Tiny (10pt) |
| Colors | White text on dark background, black text when selected |
| Spacing | DP-scaled padding and margins for cross-device consistency |
| Row height | Dynamically calculated based on screen PPI |

## Implementation Details

### Scrolling Window

Lists maintain a visible window with `start` and `end` indices:

```c
// When selection moves below visible area
if (selected >= end) {
start++;
end++;
}

// When selection moves above visible area
if (selected < start) {
start--;
end--;
}
```

### Alphabetic Index

Each directory builds an index array mapping letters (A-Z, #) to entry positions:

```c
// getIndexChar() returns 0 for non-alpha, 1-26 for A-Z
int alpha = getIndexChar(entry->name);
if (alphas[alpha] == -1) {
alphas[alpha] = entry_index; // First entry for this letter
}
```

L1/R1 navigation uses this index to jump between letter groups.

### Display Name Processing

Names go through several transformations:

1. `getDisplayName()` - Strips extensions, region codes, parentheticals, fixes articles
2. `trimSortingMeta()` - Removes "NNN) " prefixes
3. `map.txt` lookup - Applies custom aliases
4. `getUniqueName()` - Generates disambiguator for duplicates

### Sorting Algorithm

Sorting uses `strnatcasecmp()` which provides:

1. Natural number ordering ("Game 2" < "Game 10")
2. Article skipping for sort ("The Zelda" sorts under "Z")
3. Case-insensitive comparison
2 changes: 2 additions & 0 deletions makefile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ LDFLAGS = -ldl -flto $(SDL_LIBS) -lSDL2 -lSDL2_image -lSDL2_ttf -lpthread -lm -l
MINUI_SOURCE = workspace/all/minui/minui.c \
workspace/all/common/scaler.c \
workspace/all/common/utils.c \
workspace/all/common/nointro_parser.c \
workspace/all/common/api.c \
workspace/all/common/log.c \
workspace/all/common/collections.c \
workspace/all/common/pad.c \
workspace/all/common/gfx_text.c \
workspace/all/common/str_compare.c \
workspace/desktop/platform/platform.c

# Header files (dependencies)
Expand Down
35 changes: 23 additions & 12 deletions makefile.qa
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ TEST_INCLUDES = -I tests/support -I tests/support/unity -I workspace/all/common
TEST_UNITY = tests/support/unity/unity.c

# All test executables (built from tests/unit/ and tests/integration/)
TEST_EXECUTABLES = tests/utils_test tests/pad_test tests/collections_test tests/gfx_text_test tests/audio_resampler_test tests/minarch_paths_test tests/minui_utils_test tests/m3u_parser_test tests/minui_file_utils_test tests/map_parser_test tests/collection_parser_test tests/recent_parser_test tests/recent_writer_test tests/directory_utils_test tests/binary_file_utils_test tests/ui_layout_test tests/integration_workflows_test
TEST_EXECUTABLES = tests/utils_test tests/nointro_parser_test tests/pad_test tests/collections_test tests/gfx_text_test tests/audio_resampler_test tests/minarch_paths_test tests/minui_utils_test tests/m3u_parser_test tests/minui_file_utils_test tests/map_parser_test tests/collection_parser_test tests/recent_parser_test tests/recent_writer_test tests/directory_utils_test tests/binary_file_utils_test tests/ui_layout_test tests/str_compare_test tests/integration_workflows_test

# Default targets: use Docker for consistency
test: docker-test
Expand All @@ -209,22 +209,27 @@ tests/log_test: tests/unit/all/common/test_log.c workspace/all/common/log.c $(TE
@$(CC) $(TEST_INCLUDES) $(TEST_CFLAGS) -D_DEFAULT_SOURCE -o $@ $^ -lpthread

# Build comprehensive utils tests
tests/utils_test: tests/unit/all/common/test_utils.c workspace/all/common/utils.c workspace/all/common/log.c $(TEST_UNITY)
tests/utils_test: tests/unit/all/common/test_utils.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c $(TEST_UNITY)
@echo "Building comprehensive utils tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS)

# Build No-Intro name parser tests
tests/nointro_parser_test: tests/unit/all/common/test_nointro_parser.c workspace/all/common/nointro_parser.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c $(TEST_UNITY)
@echo "Building No-Intro name parser tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS)

# Build PAD (input) tests
tests/pad_test: tests/unit/all/common/test_api_pad.c workspace/all/common/pad.c $(TEST_UNITY)
@echo "Building PAD input tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS)

# Build collections (Array/Hash) tests
tests/collections_test: tests/unit/all/common/test_collections.c workspace/all/common/collections.c workspace/all/common/utils.c workspace/all/common/log.c $(TEST_UNITY)
tests/collections_test: tests/unit/all/common/test_collections.c workspace/all/common/collections.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c $(TEST_UNITY)
@echo "Building collections tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS) -D_POSIX_C_SOURCE=200809L

# Build GFX text utility tests (uses fff for TTF mocking)
tests/gfx_text_test: tests/unit/all/common/test_gfx_text.c workspace/all/common/gfx_text.c workspace/all/common/utils.c workspace/all/common/log.c tests/support/sdl_fakes.c $(TEST_UNITY)
tests/gfx_text_test: tests/unit/all/common/test_gfx_text.c workspace/all/common/gfx_text.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c tests/support/sdl_fakes.c $(TEST_UNITY)
@echo "Building GFX text utility tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) -I tests/support/fff $(TEST_CFLAGS) -DUNIT_TEST_BUILD

Expand All @@ -239,42 +244,42 @@ tests/minarch_paths_test: tests/unit/all/common/test_minarch_paths.c workspace/a
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS)

# Build LessUI launcher utility tests (pure string logic)
tests/minui_utils_test: tests/unit/all/common/test_minui_utils.c workspace/all/common/minui_utils.c workspace/all/common/utils.c workspace/all/common/log.c $(TEST_UNITY)
tests/minui_utils_test: tests/unit/all/common/test_minui_utils.c workspace/all/common/minui_utils.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c $(TEST_UNITY)
@echo "Building LessUI launcher utility tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS)

# Build M3U parser tests (uses file mocking with GCC --wrap, Docker-only)
tests/m3u_parser_test: tests/unit/all/common/test_m3u_parser.c workspace/all/common/m3u_parser.c workspace/all/common/utils.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
tests/m3u_parser_test: tests/unit/all/common/test_m3u_parser.c workspace/all/common/m3u_parser.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
@echo "Building M3U parser tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS) -D_POSIX_C_SOURCE=200809L -Wl,--wrap=exists -Wl,--wrap=fopen -Wl,--wrap=fclose -Wl,--wrap=fgets

# Build LessUI file utility tests (uses file mocking with GCC --wrap, Docker-only)
tests/minui_file_utils_test: tests/unit/all/common/test_minui_file_utils.c workspace/all/common/minui_file_utils.c workspace/all/common/utils.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
tests/minui_file_utils_test: tests/unit/all/common/test_minui_file_utils.c workspace/all/common/minui_file_utils.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
@echo "Building LessUI file utility tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS) -Wl,--wrap=exists -Wl,--wrap=fopen -Wl,--wrap=fclose -Wl,--wrap=fgets

# Build map.txt parser tests (uses file mocking with GCC --wrap, Docker-only)
tests/map_parser_test: tests/unit/all/common/test_map_parser.c workspace/all/common/map_parser.c workspace/all/common/utils.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
tests/map_parser_test: tests/unit/all/common/test_map_parser.c workspace/all/common/map_parser.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
@echo "Building map.txt parser tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS) -Wl,--wrap=exists -Wl,--wrap=fopen -Wl,--wrap=fclose -Wl,--wrap=fgets

# Build collection parser tests (uses file mocking with GCC --wrap, Docker-only)
tests/collection_parser_test: tests/unit/all/common/test_collection_parser.c workspace/all/common/collection_parser.c workspace/all/common/utils.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
tests/collection_parser_test: tests/unit/all/common/test_collection_parser.c workspace/all/common/collection_parser.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
@echo "Building collection parser tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS) -D_POSIX_C_SOURCE=200809L -Wl,--wrap=exists -Wl,--wrap=fopen -Wl,--wrap=fclose -Wl,--wrap=fgets

# Build recent.txt file tests (uses file mocking with GCC --wrap, Docker-only)
tests/recent_parser_test: tests/unit/all/common/test_recent_parser.c workspace/all/common/recent_file.c workspace/all/common/utils.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
tests/recent_parser_test: tests/unit/all/common/test_recent_parser.c workspace/all/common/recent_file.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c tests/support/fs_mocks.c $(TEST_UNITY)
@echo "Building recent.txt parser tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS) -D_POSIX_C_SOURCE=200809L -Wl,--wrap=exists -Wl,--wrap=fopen -Wl,--wrap=fclose -Wl,--wrap=fgets

# Build recent.txt writer tests (uses real temp files, no --wrap needed)
tests/recent_writer_test: tests/unit/all/common/test_recent_writer.c workspace/all/common/recent_file.c workspace/all/common/utils.c workspace/all/common/log.c $(TEST_UNITY)
tests/recent_writer_test: tests/unit/all/common/test_recent_writer.c workspace/all/common/recent_file.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c $(TEST_UNITY)
@echo "Building recent.txt writer tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS) -D_POSIX_C_SOURCE=200809L

# Build directory utility tests (uses real temp directories, no --wrap needed)
tests/directory_utils_test: tests/unit/all/common/test_directory_utils.c workspace/all/common/minui_file_utils.c workspace/all/common/utils.c workspace/all/common/log.c $(TEST_UNITY)
tests/directory_utils_test: tests/unit/all/common/test_directory_utils.c workspace/all/common/minui_file_utils.c workspace/all/common/utils.c workspace/all/common/nointro_parser.c workspace/all/common/log.c $(TEST_UNITY)
@echo "Building directory utility tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS) -D_DEFAULT_SOURCE

Expand All @@ -288,6 +293,11 @@ tests/ui_layout_test: tests/unit/all/common/test_ui_layout.c workspace/all/commo
@echo "Building UI layout / DP system tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS) -lm

# Build string comparison tests (natural sort, pure algorithm)
tests/str_compare_test: tests/unit/all/common/test_str_compare.c workspace/all/common/str_compare.c $(TEST_UNITY)
@echo "Building string comparison tests..."
@$(CC) -o $@ $^ $(TEST_INCLUDES) $(TEST_CFLAGS)

# Build integration tests (tests multiple components working together with real file I/O)
tests/integration_workflows_test: tests/integration/test_workflows.c \
tests/integration/integration_support.c \
Expand All @@ -299,6 +309,7 @@ tests/integration_workflows_test: tests/integration/test_workflows.c \
workspace/all/common/binary_file_utils.c \
workspace/all/common/minarch_paths.c \
workspace/all/common/utils.c \
workspace/all/common/nointro_parser.c \
workspace/all/common/log.c \
$(TEST_UNITY)
@echo "Building integration tests..."
Expand Down
Loading