from PySide6.QtCore import QPointF, QRectF, Qt from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygonF from bedit.core.model import Icon def icon_bounds(icon: Icon) -> QRectF: """Return the automatic hitbox of all visible vector elements.""" bounds = QRectF() for element in icon.elements: if element.get("_deleted"): continue rect = QRectF( float(element.get("x", 0)), float(element.get("y", 0)), float(element.get("width", 0)), float(element.get("height", 0)), ).normalized() bounds = rect if bounds.isNull() else bounds.united(rect) return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64) def shape_pen(element: dict) -> QPen: styles = { "solid": Qt.PenStyle.SolidLine, "dash": Qt.PenStyle.DashLine, "dot": Qt.PenStyle.DotLine, "dash-dot": Qt.PenStyle.DashDotLine, "none": Qt.PenStyle.NoPen, } return QPen( QColor(element.get("stroke", "#303030")), float(element.get("lineWidth", 1.5)), styles.get(element.get("lineStyle", "solid"), Qt.PenStyle.SolidLine), ) def paint_icon( painter: QPainter, icon: Icon, target: QRectF, source: QRectF | None = None, ) -> None: painter.save() painter.setRenderHint(QPainter.RenderHint.Antialiasing) if source is None: source = QRectF(0, 0, icon.width, icon.height) painter.translate(target.topLeft()) painter.scale(target.width() / source.width(), target.height() / source.height()) painter.translate(-source.left(), -source.top()) for element in icon.elements: kind = element.get("type", "rectangle") rect = QRectF( float(element.get("x", 0)), float(element.get("y", 0)), float(element.get("width", 20)), float(element.get("height", 20)), ) painter.setPen(shape_pen(element)) fill = element.get("fill", "#ffffff") painter.setBrush( Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill) ) if kind == "rectangle": radius = float(element.get("cornerRadius", 0)) painter.drawRoundedRect(rect, radius, radius) elif kind in {"circle", "ellipse"}: painter.drawEllipse(rect) elif kind == "line": painter.drawLine(rect.topLeft(), rect.bottomRight()) elif kind == "triangle": painter.drawPolygon( QPolygonF( [QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()] ) ) elif kind == "text": painter.setPen(QColor(element.get("color", element.get("stroke", "#202020")))) font = QFont() font.setPointSizeF(float(element.get("fontSize", 12))) painter.setFont(font) painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(element.get("text", "Text"))) painter.restore() def icon_pixmap(icon: Icon, size: int = 16) -> QPixmap: pixmap = QPixmap(size, size) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) bounds = icon_bounds(icon) ratio = min(size / bounds.width(), size / bounds.height()) width, height = bounds.width() * ratio, bounds.height() * ratio paint_icon( painter, icon, QRectF((size - width) / 2, (size - height) / 2, width, height), bounds, ) painter.end() return pixmap def library_icon(icon: Icon) -> QIcon: # Render large enough for the tall Libraries rows. icon_pixmap crops to the # vector hitbox first, so a 32x32 drawing is shown as large as a 128x128 one. return QIcon(icon_pixmap(icon, 28))