"""Overlay Widget — transparent layer rendered above the video preview.""" from __future__ import annotations from PySide6.QtCore import QRect, Qt, Slot from PySide6.QtGui import QColor, QFont, QPainter, QPen from PySide6.QtWidgets import QWidget from app.config import ( OVERLAY_BG_COLOR, OVERLAY_FONT_SIZE, OVERLAY_MARGIN, OVERLAY_PADDING, OVERLAY_TEXT_COLOR, ) from app.telemetry.telemetry_collector import TelemetrySnapshot class OverlayWidget(QWidget): """ Semi-transparent performance metrics overlay. Sits on top of the video widget (same parent, raised). Does NOT intercept mouse events — clicks pass through to the video widget. Usage: overlay = OverlayWidget(parent=main_window) telemetry_collector.metrics_updated.connect(overlay.update_metrics) """ def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) # Make widget transparent to mouse and visual background self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True) self.setWindowFlags(Qt.WindowType.FramelessWindowHint) self._snapshot: TelemetrySnapshot | None = None self._visible_overlay: bool = True # Font self._font = QFont("Monospace") self._font.setStyleHint(QFont.StyleHint.TypeWriter) self._font.setPointSize(OVERLAY_FONT_SIZE) self._font.setBold(False) # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------ @Slot(object) def update_metrics(self, snapshot: TelemetrySnapshot) -> None: """Receive a new telemetry snapshot and trigger a repaint.""" self._snapshot = snapshot if self._visible_overlay: self.update() def set_overlay_visible(self, visible: bool) -> None: self._visible_overlay = visible self.update() def toggle_overlay(self) -> None: self.set_overlay_visible(not self._visible_overlay) # ------------------------------------------------------------------ # Qt painting # ------------------------------------------------------------------ def paintEvent(self, event) -> None: # noqa: N802 if not self._visible_overlay or self._snapshot is None: return lines = self._format_lines(self._snapshot) if not lines: return painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) painter.setFont(self._font) fm = painter.fontMetrics() line_height = fm.height() max_width = max(fm.horizontalAdvance(line) for line in lines) box_w = max_width + OVERLAY_PADDING * 2 box_h = line_height * len(lines) + OVERLAY_PADDING * 2 x = OVERLAY_MARGIN y = OVERLAY_MARGIN # Background rectangle bg = QColor(*OVERLAY_BG_COLOR) painter.setBrush(bg) painter.setPen(Qt.PenStyle.NoPen) painter.drawRoundedRect(QRect(x, y, box_w, box_h), 6, 6) # Text text_color = QColor(*OVERLAY_TEXT_COLOR) painter.setPen(QPen(text_color)) text_x = x + OVERLAY_PADDING text_y = y + OVERLAY_PADDING + fm.ascent() for line in lines: painter.drawText(text_x, text_y, line) text_y += line_height painter.end() # ------------------------------------------------------------------ # Private # ------------------------------------------------------------------ @staticmethod def _format_lines(snap: TelemetrySnapshot) -> list[str]: lines = [ f"FPS {snap.fps:>6.1f}", f"Frame {snap.frame_time_ms:>6.1f} ms", f"Drop {snap.dropped_frames:>6d}", f"CPU {snap.cpu_percent:>5.1f} %", ] if snap.memory_mb is not None: lines.append(f"Mem {snap.memory_mb:>5.0f} MB") return lines