Initial Commit
This commit is contained in:
commit
f4fa723863
22 changed files with 1402 additions and 0 deletions
290
src/ui.c
Normal file
290
src/ui.c
Normal 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));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue