|
|
|
@ -18,6 +18,66 @@ if TYPE_CHECKING: |
|
|
|
from r36s_dlna_browser.dlna.browser_state import BrowserState |
|
|
|
from r36s_dlna_browser.dlna.browser_state import BrowserState |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HUDTextCache: |
|
|
|
|
|
|
|
"""Cache SDL text textures so TTF_RenderUTF8_Blended is not called every frame. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The cache maps ``(text, color_tuple)`` to ``(SDL_Texture*, full_w, full_h)``. |
|
|
|
|
|
|
|
Call ``invalidate()`` whenever the renderer or font changes (e.g. on resize |
|
|
|
|
|
|
|
or cleanup) so stale texture pointers are not used. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None: |
|
|
|
|
|
|
|
self._tex_cache: dict[tuple, tuple] = {} |
|
|
|
|
|
|
|
self._fit_cache: dict[tuple, str] = {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ |
|
|
|
|
|
|
|
def render_text( |
|
|
|
|
|
|
|
self, |
|
|
|
|
|
|
|
renderer: ctypes.c_void_p, |
|
|
|
|
|
|
|
font: ctypes.c_void_p, |
|
|
|
|
|
|
|
text: str, |
|
|
|
|
|
|
|
x: int, |
|
|
|
|
|
|
|
y: int, |
|
|
|
|
|
|
|
color: tuple, |
|
|
|
|
|
|
|
max_w: int = 0, |
|
|
|
|
|
|
|
) -> None: |
|
|
|
|
|
|
|
"""Render *text* at (x, y), reusing a cached texture when possible.""" |
|
|
|
|
|
|
|
if not text or not font or not ttf: |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
key = (text, color) |
|
|
|
|
|
|
|
entry = self._tex_cache.get(key) |
|
|
|
|
|
|
|
if entry is None: |
|
|
|
|
|
|
|
encoded = text.encode("utf-8") |
|
|
|
|
|
|
|
surf = ttf.TTF_RenderUTF8_Blended(font, encoded, _color(color)) |
|
|
|
|
|
|
|
if not surf: |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
tex = sdl2.SDL_CreateTextureFromSurface(renderer, surf) |
|
|
|
|
|
|
|
w, h = surf.contents.w, surf.contents.h |
|
|
|
|
|
|
|
sdl2.SDL_FreeSurface(surf) |
|
|
|
|
|
|
|
if not tex: |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
entry = (tex, w, h) |
|
|
|
|
|
|
|
self._tex_cache[key] = entry |
|
|
|
|
|
|
|
tex, w, h = entry |
|
|
|
|
|
|
|
if max_w and w > max_w: |
|
|
|
|
|
|
|
w = max_w |
|
|
|
|
|
|
|
sdl2.SDL_RenderCopy(renderer, tex, sdl2.SDL_Rect(0, 0, w, h), sdl2.SDL_Rect(x, y, w, h)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fit_text(self, font: ctypes.c_void_p, text: str, max_w: int) -> str: |
|
|
|
|
|
|
|
"""Cache the result of _fit_text so TTF_SizeUTF8 is not called every frame.""" |
|
|
|
|
|
|
|
key = (text, max_w) |
|
|
|
|
|
|
|
if key not in self._fit_cache: |
|
|
|
|
|
|
|
self._fit_cache[key] = _fit_text(font, text, max_w) |
|
|
|
|
|
|
|
return self._fit_cache[key] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def invalidate(self) -> None: |
|
|
|
|
|
|
|
"""Destroy all cached textures. Call when renderer or font changes.""" |
|
|
|
|
|
|
|
for tex, _w, _h in self._tex_cache.values(): |
|
|
|
|
|
|
|
sdl2.SDL_DestroyTexture(tex) |
|
|
|
|
|
|
|
self._tex_cache.clear() |
|
|
|
|
|
|
|
self._fit_cache.clear() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _color(rgba: tuple) -> sdl2.SDL_Color: |
|
|
|
def _color(rgba: tuple) -> sdl2.SDL_Color: |
|
|
|
return sdl2.SDL_Color(rgba[0], rgba[1], rgba[2], rgba[3] if len(rgba) > 3 else 255) |
|
|
|
return sdl2.SDL_Color(rgba[0], rgba[1], rgba[2], rgba[3] if len(rgba) > 3 else 255) |
|
|
|
|
|
|
|
|
|
|
|
@ -212,10 +272,25 @@ def draw_browse_list(renderer, font, state: "BrowserState", layout: theme.Layout |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def draw_playback(renderer, font, state: "BrowserState", layout: theme.Layout, icons: dict | None = None) -> None: |
|
|
|
def draw_playback( |
|
|
|
|
|
|
|
renderer, |
|
|
|
|
|
|
|
font, |
|
|
|
|
|
|
|
state: "BrowserState", |
|
|
|
|
|
|
|
layout: theme.Layout, |
|
|
|
|
|
|
|
icons: dict | None = None, |
|
|
|
|
|
|
|
*, |
|
|
|
|
|
|
|
cache: "HUDTextCache | None" = None, |
|
|
|
|
|
|
|
) -> None: |
|
|
|
if not state.playback_hud_visible: |
|
|
|
if not state.playback_hud_visible: |
|
|
|
return |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Local alias: reuse cached texture when a cache is provided, else create/destroy per call. |
|
|
|
|
|
|
|
def _rt(text: str, x: int, y: int, color: tuple, max_w: int = 0) -> None: |
|
|
|
|
|
|
|
if cache is not None: |
|
|
|
|
|
|
|
cache.render_text(renderer, font, text, x, y, color, max_w) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
_render_text(renderer, font, text, x, y, color, max_w) |
|
|
|
|
|
|
|
|
|
|
|
top_h = layout.playback_hud_top |
|
|
|
top_h = layout.playback_hud_top |
|
|
|
bottom_h = layout.playback_hud_bottom |
|
|
|
bottom_h = layout.playback_hud_bottom |
|
|
|
width = layout.width |
|
|
|
width = layout.width |
|
|
|
@ -245,44 +320,42 @@ def draw_playback(renderer, font, state: "BrowserState", layout: theme.Layout, i |
|
|
|
time_x = width - left - time_w |
|
|
|
time_x = width - left - time_w |
|
|
|
title_x = left + layout.playback_status_w + layout.playback_hud_icon_size + 8 |
|
|
|
title_x = left + layout.playback_status_w + layout.playback_hud_icon_size + 8 |
|
|
|
title_w = max(0, time_x - title_x - 12) |
|
|
|
title_w = max(0, time_x - title_x - 12) |
|
|
|
title_text = _fit_text(font, state.playback_title or "...", title_w) |
|
|
|
title_text = cache.fit_text(font, state.playback_title or "...", title_w) if cache else _fit_text(font, state.playback_title or "...", title_w) |
|
|
|
|
|
|
|
|
|
|
|
_fill_rect(renderer, 0, 0, width, top_h, theme.PLAYBACK_OVERLAY_BG) |
|
|
|
_fill_rect(renderer, 0, 0, width, top_h, theme.PLAYBACK_OVERLAY_BG) |
|
|
|
_fill_rect(renderer, 0, height - bottom_h, width, bottom_h, theme.PLAYBACK_OVERLAY_BG) |
|
|
|
_fill_rect(renderer, 0, height - bottom_h, width, bottom_h, theme.PLAYBACK_OVERLAY_BG) |
|
|
|
_render_icon(renderer, status_icon, left, max(5, top_h // 5), layout.playback_hud_icon_size) |
|
|
|
_render_icon(renderer, status_icon, left, max(5, top_h // 5), layout.playback_hud_icon_size) |
|
|
|
_render_text(renderer, font, status, left + layout.playback_hud_icon_size + 6, max(4, top_h // 6), theme.TEXT_COLOR, layout.playback_status_w) |
|
|
|
_rt(status, left + layout.playback_hud_icon_size + 6, max(4, top_h // 6), theme.TEXT_COLOR, layout.playback_status_w) |
|
|
|
_render_text(renderer, font, title_text, |
|
|
|
_rt(title_text, title_x, 5, theme.TEXT_COLOR, title_w) |
|
|
|
title_x, 5, theme.TEXT_COLOR, title_w) |
|
|
|
_rt(time_text, time_x, 5, theme.DIM_TEXT, time_w) |
|
|
|
_render_text(renderer, font, time_text, |
|
|
|
|
|
|
|
time_x, 5, theme.DIM_TEXT, time_w) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
progress_y = height - bottom_h + 10 |
|
|
|
progress_y = height - bottom_h + 10 |
|
|
|
_draw_progress_bar(renderer, progress_x, progress_y, progress_w, layout.playback_progress_h, progress) |
|
|
|
_draw_progress_bar(renderer, progress_x, progress_y, progress_w, layout.playback_progress_h, progress) |
|
|
|
|
|
|
|
|
|
|
|
info_y = height - bottom_h + max(20, bottom_h // 2 - layout.playback_hud_icon_size // 2) |
|
|
|
info_y = height - bottom_h + max(20, bottom_h // 2 - layout.playback_hud_icon_size // 2) |
|
|
|
_render_icon(renderer, volume_icon, left, info_y + 1, layout.playback_hud_icon_size) |
|
|
|
_render_icon(renderer, volume_icon, left, info_y + 1, layout.playback_hud_icon_size) |
|
|
|
_render_text(renderer, font, f"{state.playback_volume}%", left + 22, info_y - 1, theme.TEXT_COLOR, 64) |
|
|
|
_rt(f"{state.playback_volume}%", left + 22, info_y - 1, theme.TEXT_COLOR, 64) |
|
|
|
|
|
|
|
|
|
|
|
buffer_text = f"Buffer {state.playback_buffer_percent}%" if state.playback_buffer_percent < 100 else state.playback_resolution or "" |
|
|
|
buffer_text = f"Buffer {state.playback_buffer_percent}%" if state.playback_buffer_percent < 100 else state.playback_resolution or "" |
|
|
|
_render_text(renderer, font, buffer_text, left + max(88, layout.playback_status_w + 18), info_y - 1, theme.DIM_TEXT, layout.playback_buffer_w) |
|
|
|
_rt(buffer_text, left + max(88, layout.playback_status_w + 18), info_y - 1, theme.DIM_TEXT, layout.playback_buffer_w) |
|
|
|
_render_text(renderer, font, hud_mode, width - 70, info_y - 1, theme.DIM_TEXT, 60) |
|
|
|
_rt(hud_mode, width - 70, info_y - 1, theme.DIM_TEXT, 60) |
|
|
|
|
|
|
|
|
|
|
|
controls_y = height - layout.playback_bottom_margin |
|
|
|
controls_y = height - layout.playback_bottom_margin |
|
|
|
cx = left |
|
|
|
cx = left |
|
|
|
_render_icon(renderer, volume_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_icon(renderer, volume_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_text(renderer, font, "U/D", cx + 20, controls_y - 2, theme.TEXT_COLOR, 38) |
|
|
|
_rt("U/D", cx + 20, controls_y - 2, theme.TEXT_COLOR, 38) |
|
|
|
cx += 70 |
|
|
|
cx += 70 |
|
|
|
_render_icon(renderer, status_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_icon(renderer, status_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_text(renderer, font, "A", cx + 20, controls_y - 2, theme.TEXT_COLOR, 18) |
|
|
|
_rt("A", cx + 20, controls_y - 2, theme.TEXT_COLOR, 18) |
|
|
|
cx += 44 |
|
|
|
cx += 44 |
|
|
|
_render_icon(renderer, seek_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_icon(renderer, seek_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_text(renderer, font, "L/R", cx + 20, controls_y - 2, theme.TEXT_COLOR, 34) |
|
|
|
_rt("L/R", cx + 20, controls_y - 2, theme.TEXT_COLOR, 34) |
|
|
|
cx += 62 |
|
|
|
cx += 62 |
|
|
|
_render_icon(renderer, stop_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_icon(renderer, stop_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_text(renderer, font, "B", cx + 20, controls_y - 2, theme.TEXT_COLOR, 18) |
|
|
|
_rt("B", cx + 20, controls_y - 2, theme.TEXT_COLOR, 18) |
|
|
|
cx += 42 |
|
|
|
cx += 42 |
|
|
|
_render_icon(renderer, hud_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_icon(renderer, hud_icon, cx, controls_y - 1, layout.playback_hud_icon_size) |
|
|
|
_render_text(renderer, font, "Y", cx + 20, controls_y - 2, theme.TEXT_COLOR, 18) |
|
|
|
_rt("Y", cx + 20, controls_y - 2, theme.TEXT_COLOR, 18) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def draw_error(renderer, font, state: "BrowserState", layout: theme.Layout) -> None: |
|
|
|
def draw_error(renderer, font, state: "BrowserState", layout: theme.Layout) -> None: |
|
|
|
|