"""Vector icons drawn with QPainter (one consistent style, no image files, no emoji). `icon(name, color)` returns a QIcon rendered at several sizes. Names: play, stop, record, open, apply, revert, save, export, import, settings, star, star-off, warning, ab, loop, search, wave. `app_icon()` uses the artwork in app/assets (made by tools/make_assets.py from the user's images). """ import math import os from PySide6.QtCore import QPointF, QRectF, Qt from PySide6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPen, QPixmap ACCENT = "#b0242c" STEEL = "#c4c4c4" TEXT = "#e6e6e6" DANGER = "#e0533d" MUTED = "#9a9a9a" _cache = {} def _arrow_arc(p, r, start, span, head=True): rect = QRectF(-r, -r, 2 * r, 2 * r) path = QPainterPath() path.arcMoveTo(rect, start) path.arcTo(rect, start, span) p.drawPath(path) if head: a = math.radians(start + span) tip = QPointF(r * math.cos(a), -r * math.sin(a)) d = 1 if span > 0 else -1 tang = math.radians(start + span + 90 * d) back = QPointF(math.cos(tang), -math.sin(tang)) nrm = QPointF(math.cos(a), -math.sin(a)) L = r * 0.55 p.drawLine(tip, tip - back * L + nrm * L * 0.6) p.drawLine(tip, tip - back * L - nrm * L * 0.6) def _draw(p, name, c, s): """Draw icon `name` in a box of size s (origin top-left).""" pen = QPen(QColor(c), max(1.2, s * 0.09), Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin) p.setPen(pen) p.setBrush(Qt.BrushStyle.NoBrush) m = s * 0.18 if name == "play": path = QPainterPath() path.moveTo(s * 0.30, s * 0.20) path.lineTo(s * 0.80, s * 0.50) path.lineTo(s * 0.30, s * 0.80) path.closeSubpath() p.setBrush(QColor(c)) p.drawPath(path) elif name == "stop": p.setBrush(QColor(c)) p.drawRoundedRect(QRectF(s * 0.26, s * 0.26, s * 0.48, s * 0.48), s * 0.06, s * 0.06) elif name == "record": p.setPen(Qt.PenStyle.NoPen) p.setBrush(QColor(DANGER)) p.drawEllipse(QRectF(s * 0.22, s * 0.22, s * 0.56, s * 0.56)) elif name == "open": path = QPainterPath() path.moveTo(m, s * 0.30) path.lineTo(s * 0.40, s * 0.30) path.lineTo(s * 0.48, s * 0.38) path.lineTo(s - m, s * 0.38) path.lineTo(s - m, s * 0.78) path.lineTo(m, s * 0.78) path.closeSubpath() p.drawPath(path) elif name == "apply": path = QPainterPath() path.moveTo(s * 0.20, s * 0.52) path.lineTo(s * 0.42, s * 0.74) path.lineTo(s * 0.82, s * 0.28) p.drawPath(path) elif name in ("revert", "loop"): p.translate(s / 2, s / 2) if name == "revert": _arrow_arc(p, s * 0.30, 60, 270) else: _arrow_arc(p, s * 0.30, 20, 150) _arrow_arc(p, s * 0.30, 200, 150) p.translate(-s / 2, -s / 2) elif name == "save": # disk path = QPainterPath() path.moveTo(m, m) path.lineTo(s * 0.70, m) path.lineTo(s - m, s * 0.30) path.lineTo(s - m, s - m) path.lineTo(m, s - m) path.closeSubpath() p.drawPath(path) p.drawRect(QRectF(s * 0.32, m, s * 0.30, s * 0.20)) p.drawRect(QRectF(s * 0.30, s * 0.56, s * 0.40, s * 0.26)) elif name in ("export", "import"): p.drawLine(QPointF(m, s * 0.62), QPointF(m, s - m)) p.drawLine(QPointF(m, s - m), QPointF(s - m, s - m)) p.drawLine(QPointF(s - m, s - m), QPointF(s - m, s * 0.62)) up = name == "export" a, b = (s * 0.66, s * 0.16) if up else (s * 0.16, s * 0.66) p.drawLine(QPointF(s / 2, a), QPointF(s / 2, b)) d = -1 if up else 1 p.drawLine(QPointF(s / 2, b), QPointF(s / 2 - s * 0.16, b - d * s * 0.16)) p.drawLine(QPointF(s / 2, b), QPointF(s / 2 + s * 0.16, b - d * s * 0.16)) elif name == "settings": p.translate(s / 2, s / 2) path = QPainterPath() teeth = 8 for i in range(teeth * 2): a = math.pi * i / teeth r = s * (0.36 if i % 2 == 0 else 0.27) pt = QPointF(r * math.cos(a), r * math.sin(a)) path.moveTo(pt) if i == 0 else path.lineTo(pt) path.closeSubpath() p.drawPath(path) p.drawEllipse(QPointF(0, 0), s * 0.11, s * 0.11) p.translate(-s / 2, -s / 2) elif name in ("star", "star-off"): path = QPainterPath() for i in range(10): a = -math.pi / 2 + math.pi * i / 5 r = s * (0.40 if i % 2 == 0 else 0.17) pt = QPointF(s / 2 + r * math.cos(a), s * 0.54 + r * math.sin(a)) path.moveTo(pt) if i == 0 else path.lineTo(pt) path.closeSubpath() if name == "star": p.setBrush(QColor(c)) p.drawPath(path) elif name == "warning": path = QPainterPath() path.moveTo(s / 2, s * 0.14) path.lineTo(s * 0.88, s * 0.84) path.lineTo(s * 0.12, s * 0.84) path.closeSubpath() p.drawPath(path) p.drawLine(QPointF(s / 2, s * 0.40), QPointF(s / 2, s * 0.60)) p.drawPoint(QPointF(s / 2, s * 0.72)) elif name == "ab": # compare: half-filled circle rect = QRectF(s * 0.18, s * 0.18, s * 0.64, s * 0.64) half = QPainterPath() half.moveTo(s / 2, s * 0.18) half.arcTo(rect, 90, 180) half.closeSubpath() p.setBrush(QColor(c)) p.drawPath(half) p.setBrush(Qt.BrushStyle.NoBrush) p.drawEllipse(rect) elif name in ("prev", "next"): # chevron d = -1 if name == "prev" else 1 x0 = s / 2 - d * s * 0.12 p.drawLine(QPointF(x0, s * 0.24), QPointF(x0 + d * s * 0.24, s / 2)) p.drawLine(QPointF(x0 + d * s * 0.24, s / 2), QPointF(x0, s * 0.76)) elif name == "add": # plus p.drawLine(QPointF(s / 2, s * 0.22), QPointF(s / 2, s * 0.78)) p.drawLine(QPointF(s * 0.22, s / 2), QPointF(s * 0.78, s / 2)) elif name == "edit": # pencil p.translate(s / 2, s / 2) p.rotate(-45) p.drawRect(QRectF(-s * 0.30, -s * 0.09, s * 0.48, s * 0.18)) path = QPainterPath() path.moveTo(s * 0.18, -s * 0.09) path.lineTo(s * 0.36, 0) path.lineTo(s * 0.18, s * 0.09) p.drawPath(path) p.rotate(45) p.translate(-s / 2, -s / 2) elif name == "search": p.drawEllipse(QRectF(s * 0.18, s * 0.18, s * 0.46, s * 0.46)) p.drawLine(QPointF(s * 0.58, s * 0.58), QPointF(s * 0.84, s * 0.84)) elif name == "wave": # fallback app glyph: waveform bars p.setPen(Qt.PenStyle.NoPen) p.setBrush(QColor(c)) for i, h in enumerate((0.25, 0.5, 0.8, 0.45, 0.65, 0.3)): p.drawRoundedRect(QRectF(s * (0.14 + i * 0.13), s * (0.5 - h / 2), s * 0.08, s * h), s * 0.03, s * 0.03) def icon(name, color=TEXT): key = (name, color) if key not in _cache: ic = QIcon() for sz in (16, 20, 24, 32, 48, 64, 128, 256): pm = QPixmap(sz, sz) pm.fill(Qt.GlobalColor.transparent) p = QPainter(pm) p.setRenderHint(QPainter.RenderHint.Antialiasing) _draw(p, name, color, sz) p.end() ic.addPixmap(pm) _cache[key] = ic return _cache[key] ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets") def asset(name): p = os.path.join(ASSETS, name) return p if os.path.exists(p) else None def app_icon(): """The app icon (Arkham City logo bat sending out sound arcs); a plain waveform glyph if the assets are missing.""" p = asset("app_icon.ico") or asset("app_icon.png") return QIcon(p) if p else icon("wave", ACCENT)