"""Icons drawn with QPainter rather than shipped as files or borrowed from an emoji font — crisp at any DPI, recolourable, and no assets to lose. `paint(painter, name, rect, colour)` draws into a rect; `icon(name, colour)` wraps that as a QIcon; `IconLabel` is a tiny widget that shows one. """ from __future__ import annotations from PySide6.QtCore import QBuffer, QByteArray, QIODevice, QPointF, QRectF, QSize, Qt from PySide6.QtGui import QColor, QIcon, QImage, QPainter, QPainterPath, QPen, QPixmap from PySide6.QtWidgets import QWidget def _pen(colour: QColor, width: float) -> QPen: pen = QPen(colour, width) pen.setCapStyle(Qt.RoundCap) pen.setJoinStyle(Qt.RoundJoin) return pen def paint(p: QPainter, name: str, rect: QRectF, colour) -> None: colour = QColor(colour) p.save() p.setRenderHint(QPainter.Antialiasing) # Everything below is drawn on a 24x24 grid, then scaled into *rect*. side = min(rect.width(), rect.height()) p.translate(rect.center().x() - side / 2, rect.center().y() - side / 2) p.scale(side / 24.0, side / 24.0) stroke = _pen(colour, 1.9) if name == "bolt": path = QPainterPath() for i, (x, y) in enumerate([(13.5, 2), (4.5, 13.5), (11, 13.5), (9.5, 22), (19.5, 9.5), (13, 9.5), (13.5, 2)]): path.moveTo(x, y) if i == 0 else path.lineTo(x, y) p.setPen(Qt.NoPen) p.setBrush(colour) p.drawPath(path) elif name == "send": p.setPen(_pen(colour, 2.4)) p.drawLine(QPointF(12, 19), QPointF(12, 5.5)) p.drawLine(QPointF(6.5, 11), QPointF(12, 5.5)) p.drawLine(QPointF(17.5, 11), QPointF(12, 5.5)) elif name == "folder": p.setPen(stroke) path = QPainterPath() path.moveTo(3, 7) path.lineTo(3, 18.5) path.quadTo(3, 20, 4.5, 20) path.lineTo(19.5, 20) path.quadTo(21, 20, 21, 18.5) path.lineTo(21, 9.5) path.quadTo(21, 8, 19.5, 8) path.lineTo(11.5, 8) path.lineTo(9.5, 5.5) path.lineTo(4.5, 5.5) path.quadTo(3, 5.5, 3, 7) p.drawPath(path) elif name == "shell": p.setPen(stroke) p.drawRoundedRect(QRectF(2.5, 4, 19, 16), 3, 3) p.drawLine(QPointF(7, 10), QPointF(10, 12.5)) p.drawLine(QPointF(10, 12.5), QPointF(7, 15)) p.drawLine(QPointF(12.5, 15.5), QPointF(17, 15.5)) elif name in ("read", "write", "edit"): p.setPen(stroke) page = QPainterPath() page.moveTo(14, 2.5) page.lineTo(6, 2.5) page.quadTo(4.5, 2.5, 4.5, 4) page.lineTo(4.5, 20) page.quadTo(4.5, 21.5, 6, 21.5) page.lineTo(18, 21.5) page.quadTo(19.5, 21.5, 19.5, 20) page.lineTo(19.5, 8) page.closeSubpath() p.drawPath(page) p.drawLine(QPointF(14, 2.5), QPointF(14, 8)) p.drawLine(QPointF(14, 8), QPointF(19.5, 8)) if name == "read": for y in (12, 15, 18): p.drawLine(QPointF(8, y), QPointF(16, y)) elif name == "write": p.drawLine(QPointF(12, 11.5), QPointF(12, 18)) p.drawLine(QPointF(8.75, 14.75), QPointF(15.25, 14.75)) else: p.drawLine(QPointF(8, 18), QPointF(15.5, 10.5)) p.drawLine(QPointF(8, 18), QPointF(8, 15.5)) p.drawLine(QPointF(8, 18), QPointF(10.5, 18)) elif name == "list": p.setPen(stroke) for y in (6.5, 12, 17.5): p.drawPoint(QPointF(4.5, y)) p.drawLine(QPointF(8.5, y), QPointF(20, y)) elif name == "blocked": p.setPen(stroke) p.drawEllipse(QRectF(3.5, 3.5, 17, 17)) p.drawLine(QPointF(7, 17), QPointF(17, 7)) elif name == "chevron": p.setPen(stroke) p.drawLine(QPointF(9, 6), QPointF(15, 12)) p.drawLine(QPointF(15, 12), QPointF(9, 18)) elif name == "chats": p.setPen(stroke) bubble = QPainterPath() bubble.addRoundedRect(QRectF(3, 4, 18, 13), 4, 4) p.drawPath(bubble) p.drawLine(QPointF(8, 17), QPointF(7, 21)) p.drawLine(QPointF(7, 21), QPointF(12, 17)) for x in (8, 12, 16): p.drawPoint(QPointF(x, 10.5)) elif name == "mic": p.setPen(stroke) p.drawRoundedRect(QRectF(9, 3, 6, 11.5), 3, 3) arc = QPainterPath() arc.moveTo(5.5, 11) arc.cubicTo(5.5, 19, 18.5, 19, 18.5, 11) p.drawPath(arc) p.drawLine(QPointF(12, 17), QPointF(12, 21)) elif name == "stop": p.setPen(Qt.NoPen) p.setBrush(colour) p.drawRoundedRect(QRectF(7, 7, 10, 10), 2, 2) elif name == "plus": p.setPen(_pen(colour, 2.1)) p.drawLine(QPointF(12, 5), QPointF(12, 19)) p.drawLine(QPointF(5, 12), QPointF(19, 12)) elif name == "file": paint(p, "read", QRectF(0, 0, 24, 24), colour) p.restore() def icon(name: str, colour, size: int = 32) -> QIcon: pixmap = QPixmap(size, size) pixmap.fill(Qt.transparent) painter = QPainter(pixmap) paint(painter, name, QRectF(0, 0, size, size), colour) painter.end() return QIcon(pixmap) APP_ICON_SIZES = (16, 24, 32, 48, 64, 128, 256) def app_icon_image(size: int) -> QImage: """An amber rounded square with a dark bolt, at *size* pixels. The one drawing behind both the window icon and the .exe's icon (write_ico).""" image = QImage(size, size, QImage.Format_ARGB32_Premultiplied) image.fill(Qt.transparent) p = QPainter(image) p.setRenderHint(QPainter.Antialiasing) p.setPen(Qt.NoPen) p.setBrush(QColor("#f5b83d")) p.drawRoundedRect(QRectF(0, 0, size, size), size * 0.26, size * 0.26) inset = size * 0.18 paint(p, "bolt", QRectF(inset, inset, size - 2 * inset, size - 2 * inset), "#1a1305") p.end() return image def app_icon() -> QIcon: """The window/taskbar icon.""" result = QIcon() for size in APP_ICON_SIZES: result.addPixmap(QPixmap.fromImage(app_icon_image(size))) return result def write_ico(path) -> None: """Save the app icon as a multi-size Windows .ico (PNG-compressed entries, which Windows Vista+ reads at every size), for the frozen .exe.""" import struct blobs = [] for size in APP_ICON_SIZES: data = QByteArray() buffer = QBuffer(data) buffer.open(QIODevice.WriteOnly) app_icon_image(size).save(buffer, "PNG") buffer.close() blobs.append((size, bytes(data))) header = struct.pack("= 256 else size # 0 means 256 in the ICO format entries += struct.pack(" None: self._name = name if colour is not None: self._colour = QColor(colour) self.update() def paintEvent(self, _event) -> None: p = QPainter(self) p.setRenderHint(QPainter.Antialiasing) rect = QRectF(self.rect()) if self._tile is not None: p.setPen(Qt.NoPen) p.setBrush(QColor(self._tile)) p.drawRoundedRect(rect, rect.width() * 0.28, rect.width() * 0.28) pad = rect.width() * 0.22 rect = rect.adjusted(pad, pad, -pad, -pad) paint(p, self._name, rect, self._colour)