#include "ui.h" #include #include 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)); }