Initial Commit

This commit is contained in:
markmental 2026-03-27 21:45:40 -04:00
commit f4fa723863
22 changed files with 1402 additions and 0 deletions

290
src/ui.c Normal file
View file

@ -0,0 +1,290 @@
#include "ui.h"
#include <stdio.h>
#include <string.h>
static const char *FONT_CANDIDATES[] = {
"BigBlueTermPlus Nerd Font Mono.ttf",
"BigBlueTermPlusNerdFontMono-Regular.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/liberation2/LiberationMono-Regular.ttf"
};
static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color);
static void set_draw_color(SDL_Renderer *renderer, SDL_Color color) {
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
}
static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, const char *text, SDL_Color color, int *width, int *height) {
SDL_Surface *surface;
SDL_Texture *texture;
if (!font || !text || text[0] == '\0') {
return NULL;
}
surface = TTF_RenderUTF8_Blended(font, text, color);
if (!surface) {
return NULL;
}
texture = SDL_CreateTextureFromSurface(renderer, surface);
if (width) {
*width = surface->w;
}
if (height) {
*height = surface->h;
}
SDL_FreeSurface(surface);
return texture;
}
static void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, int x, int y, SDL_Color color) {
SDL_Texture *texture;
SDL_Rect dst;
int width = 0;
int height = 0;
texture = text_to_texture(renderer, font, text, color, &width, &height);
if (!texture) {
return;
}
dst.x = x;
dst.y = y;
dst.w = width;
dst.h = height;
SDL_RenderCopy(renderer, texture, NULL, &dst);
SDL_DestroyTexture(texture);
}
static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
set_draw_color(renderer, color);
SDL_RenderFillRect(renderer, rect);
}
static void stroke_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
set_draw_color(renderer, color);
SDL_RenderDrawRect(renderer, rect);
}
static SDL_Rect fit_rect(int outer_x, int outer_y, int outer_w, int outer_h, int inner_w, int inner_h) {
SDL_Rect dst = {outer_x, outer_y, outer_w, outer_h};
double scale;
if (inner_w <= 0 || inner_h <= 0) {
return dst;
}
scale = SDL_min((double) outer_w / (double) inner_w, (double) outer_h / (double) inner_h);
dst.w = (int) (inner_w * scale);
dst.h = (int) (inner_h * scale);
dst.x = outer_x + (outer_w - dst.w) / 2;
dst.y = outer_y + (outer_h - dst.h) / 2;
return dst;
}
static void draw_video(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height, SDL_Rect destination) {
if (!video_texture) {
fill_rect(renderer, &destination, COLOR_BLACK);
return;
}
{
SDL_Rect fitted = fit_rect(destination.x, destination.y, destination.w, destination.h, texture_width, texture_height);
fill_rect(renderer, &destination, COLOR_BLACK);
SDL_RenderCopy(renderer, video_texture, NULL, &fitted);
}
}
static void format_clock_label(char *buffer, size_t buffer_size, time_t now, int minute_offset) {
struct tm local_time;
time_t slot = now + minute_offset * 60;
localtime_r(&slot, &local_time);
strftime(buffer, buffer_size, "%I:%M %p", &local_time);
}
static void draw_timeline_header(SDL_Renderer *renderer, const UiFonts *fonts, SDL_Rect rect, time_t now) {
char label[32];
int segments = 4;
draw_text(renderer, fonts->medium, "FRI 6/30", rect.x + 12, rect.y + 8, COLOR_TEXT_DARK);
for (int i = 0; i < segments; ++i) {
int x = rect.x + (rect.w * i) / segments;
format_clock_label(label, sizeof(label), now, (int) ((TIMELINE_VISIBLE_SECONDS / 60.0 / segments) * i));
draw_text(renderer, fonts->small, label, x + 6, rect.y + 10, COLOR_TEXT_DARK);
set_draw_color(renderer, COLOR_SLATE);
SDL_RenderDrawLine(renderer, x, rect.y + rect.h - 4, x, rect.y + rect.h + 5 * 76);
}
}
static void draw_footer_legend(SDL_Renderer *renderer, const UiFonts *fonts, int window_width, int window_height) {
SDL_Rect footer = {0, window_height - 54, window_width, 54};
SDL_Rect chip = {window_width / 2 - 170, window_height - 40, 24, 24};
fill_rect(renderer, &footer, COLOR_PALE_BLUE);
set_draw_color(renderer, COLOR_SLATE);
SDL_RenderDrawLine(renderer, footer.x, footer.y, footer.x + footer.w, footer.y);
fill_rect(renderer, &chip, COLOR_HIGHLIGHT_YELLOW);
draw_text(renderer, fonts->medium, "A", chip.x + 7, chip.y + 2, COLOR_TEXT_DARK);
draw_text(renderer, fonts->medium, "Time", chip.x + 34, chip.y + 1, COLOR_TEXT_DARK);
chip.x += 132;
fill_rect(renderer, &chip, COLOR_DEEP_BLUE);
draw_text(renderer, fonts->medium, "B", chip.x + 7, chip.y + 2, COLOR_TEXT_LIGHT);
draw_text(renderer, fonts->medium, "Theme", chip.x + 34, chip.y + 1, COLOR_TEXT_DARK);
chip.x += 136;
fill_rect(renderer, &chip, COLOR_HINT_RED);
draw_text(renderer, fonts->medium, "C", chip.x + 7, chip.y + 2, COLOR_TEXT_LIGHT);
draw_text(renderer, fonts->medium, "Title", chip.x + 34, chip.y + 1, COLOR_TEXT_DARK);
}
void ui_render_no_media(SDL_Renderer *renderer, const UiFonts *fonts) {
SDL_Rect full = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT};
fill_rect(renderer, &full, COLOR_NAVY_DARK);
draw_text(renderer, fonts->large, "Passport-C Media Player", 58, 80, COLOR_TEXT_LIGHT);
draw_text(renderer, fonts->medium, "No channels found in ./media", 58, 136, COLOR_HIGHLIGHT_YELLOW);
draw_text(renderer, fonts->small, "Add MP4 or MKV files to ./media and relaunch.", 58, 176, COLOR_PALE_BLUE);
}
void ui_render_fullscreen(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height) {
SDL_Rect window = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT};
fill_rect(renderer, &window, COLOR_BLACK);
draw_video(renderer, video_texture, texture_width, texture_height, window);
}
void ui_render_guide(SDL_Renderer *renderer,
SDL_Texture *video_texture,
int texture_width,
int texture_height,
const UiFonts *fonts,
const ChannelList *channels,
int active_channel,
time_t app_start_time,
time_t now) {
SDL_Rect full = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT};
SDL_Rect header_card = {0, 0, 390, 168};
SDL_Rect preview = {820, 0, 460, 210};
SDL_Rect header_row = {0, 210, WINDOW_WIDTH, 42};
SDL_Rect grid = {0, 252, WINDOW_WIDTH, WINDOW_HEIGHT - 306};
int row_height = grid.h / GUIDE_VISIBLE_ROWS;
int sidebar_width = 240;
int timeline_x = sidebar_width + 8;
int timeline_w = WINDOW_WIDTH - timeline_x - 12;
int start_index = active_channel - 2;
char detail[256];
fill_rect(renderer, &full, COLOR_NAVY_DARK);
fill_rect(renderer, &header_card, COLOR_PALE_BLUE);
fill_rect(renderer, &preview, COLOR_BLACK);
draw_video(renderer, video_texture, texture_width, texture_height, preview);
fill_rect(renderer, &header_row, COLOR_SLATE);
if (channels && channels->count > 0 && active_channel >= 0 && active_channel < channels->count) {
const Channel *current = &channels->items[active_channel];
snprintf(detail, sizeof(detail), "%.0f min - %s", current->duration_seconds / 60.0, current->file_name);
fill_rect(renderer, &(SDL_Rect){0, 0, 390, 38}, COLOR_HIGHLIGHT_YELLOW);
draw_text(renderer, fonts->large, current->name, 26, 44, COLOR_TEXT_DARK);
draw_text(renderer, fonts->medium, current->program_title, 20, 92, COLOR_TEXT_DARK);
draw_text(renderer, fonts->small, detail, 20, 124, COLOR_TEXT_DARK);
draw_text(renderer, fonts->medium, "Nick * 56", preview.x + preview.w - 126, preview.y + preview.h - 34, COLOR_TEXT_LIGHT);
}
draw_timeline_header(renderer, fonts, header_row, now);
if (start_index < 0) {
start_index = 0;
}
if (channels && start_index + GUIDE_VISIBLE_ROWS > channels->count) {
start_index = SDL_max(0, channels->count - GUIDE_VISIBLE_ROWS);
}
for (int row = 0; row < GUIDE_VISIBLE_ROWS; ++row) {
int channel_index = start_index + row;
SDL_Rect row_rect = {0, grid.y + row * row_height, WINDOW_WIDTH, row_height};
SDL_Rect sidebar = {0, row_rect.y, sidebar_width, row_height};
SDL_Rect timeline_rect = {timeline_x, row_rect.y + 6, timeline_w, row_height - 12};
SDL_Rect clip = timeline_rect;
int is_selected = channel_index == active_channel;
fill_rect(renderer, &row_rect, is_selected ? COLOR_DEEP_BLUE : COLOR_NAVY_DARK);
fill_rect(renderer, &sidebar, is_selected ? COLOR_DEEP_BLUE : COLOR_SLATE);
set_draw_color(renderer, COLOR_PALE_BLUE);
SDL_RenderDrawLine(renderer, row_rect.x, row_rect.y, row_rect.x + row_rect.w, row_rect.y);
if (!channels || channel_index >= channels->count) {
continue;
}
{
const Channel *channel = &channels->items[channel_index];
char number[16];
double live_position = channel_live_position(channel, app_start_time, now);
double pixels_per_second = timeline_w / TIMELINE_VISIBLE_SECONDS;
int block_x = timeline_x - (int) (live_position * pixels_per_second);
int block_w = (int) (channel->duration_seconds * pixels_per_second);
SDL_Rect block = {block_x, timeline_rect.y + 4, SDL_max(block_w, 48), timeline_rect.h - 8};
snprintf(number, sizeof(number), "%d", channel->number);
draw_text(renderer, fonts->medium, channel->name, 20, row_rect.y + 12, COLOR_TEXT_LIGHT);
draw_text(renderer, fonts->medium, number, 176, row_rect.y + 12, COLOR_TEXT_LIGHT);
draw_text(renderer, fonts->small, channel->file_name, 20, row_rect.y + 38, COLOR_PALE_BLUE);
fill_rect(renderer, &timeline_rect, COLOR_NAVY_DARK);
SDL_RenderSetClipRect(renderer, &clip);
fill_rect(renderer, &block, is_selected ? COLOR_HIGHLIGHT_YELLOW : COLOR_DEEP_BLUE);
stroke_rect(renderer, &block, COLOR_PALE_BLUE);
draw_text(renderer,
is_selected ? fonts->medium : fonts->small,
channel->program_title,
block.x + 10,
block.y + 10,
is_selected ? COLOR_TEXT_DARK : COLOR_TEXT_LIGHT);
SDL_RenderSetClipRect(renderer, NULL);
stroke_rect(renderer, &timeline_rect, COLOR_SLATE);
}
}
draw_footer_legend(renderer, fonts, WINDOW_WIDTH, WINDOW_HEIGHT);
}
int ui_load_fonts(UiFonts *fonts) {
if (!fonts) {
return -1;
}
memset(fonts, 0, sizeof(*fonts));
for (size_t i = 0; i < sizeof(FONT_CANDIDATES) / sizeof(FONT_CANDIDATES[0]); ++i) {
fonts->small = TTF_OpenFont(FONT_CANDIDATES[i], 18);
fonts->medium = TTF_OpenFont(FONT_CANDIDATES[i], 24);
fonts->large = TTF_OpenFont(FONT_CANDIDATES[i], 32);
if (fonts->small && fonts->medium && fonts->large) {
return 0;
}
ui_destroy_fonts(fonts);
}
return -1;
}
void ui_destroy_fonts(UiFonts *fonts) {
if (!fonts) {
return;
}
if (fonts->small) {
TTF_CloseFont(fonts->small);
}
if (fonts->medium) {
TTF_CloseFont(fonts->medium);
}
if (fonts->large) {
TTF_CloseFont(fonts->large);
}
memset(fonts, 0, sizeof(*fonts));
}