vibefpmgui.icons
Small vector icons for the command toolbar, drawn with QPainter so they match the current text color (light or dark mode) at any screen scale.
1"""Small vector icons for the command toolbar, drawn with QPainter so they 2match the current text color (light or dark mode) at any screen scale.""" 3 4from __future__ import annotations 5 6import math 7import re 8from functools import cache 9from pathlib import Path 10 11from qtpy.QtCore import QPointF, QRectF, Qt 12from qtpy.QtGui import ( 13 QColor, 14 QIcon, 15 QLinearGradient, 16 QPainter, 17 QPainterPath, 18 QPen, 19 QPixmap, 20 QPolygonF, 21) 22 23SIZE = 24 # logical size; drawn at several pixel ratios for crisp HiDPI 24 25 26def _canvas(ratio: float) -> tuple[QPixmap, QPainter]: 27 pixmap = QPixmap(int(SIZE * ratio), int(SIZE * ratio)) 28 pixmap.setDevicePixelRatio(ratio) 29 pixmap.fill(Qt.transparent) 30 painter = QPainter(pixmap) 31 painter.setRenderHint(QPainter.Antialiasing) 32 return pixmap, painter 33 34 35def _pen(color: QColor, width: float = 1.8) -> QPen: 36 return QPen(color, width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) 37 38 39def _hammer(p: QPainter, c: QColor): 40 p.translate(12, 12) 41 p.rotate(-45) 42 p.setPen(Qt.NoPen) 43 p.setBrush(c) 44 p.drawRoundedRect(QRectF(-1.3, -3, 2.6, 13), 1.2, 1.2) # handle 45 head = QPainterPath() 46 head.addRoundedRect(QRectF(-6.5, -8, 11, 5.5), 1.5, 1.5) 47 p.drawPath(head) 48 p.drawRoundedRect(QRectF(4, -7.2, 2.6, 3.9), 0.8, 0.8) # striking face 49 50 51def _play(p: QPainter, c: QColor): 52 path = QPainterPath(QPointF(8, 5.5)) 53 path.lineTo(18.5, 12) 54 path.lineTo(8, 18.5) 55 path.closeSubpath() 56 p.setPen(QPen(c, 2.2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) 57 p.setBrush(c) 58 p.drawPath(path) 59 60 61def _flask(p: QPainter, c: QColor): 62 body = QPainterPath(QPointF(9.5, 4)) 63 body.lineTo(9.5, 10) 64 body.lineTo(4.5, 18.2) 65 body.quadTo(3.6, 20, 5.6, 20) 66 body.lineTo(18.4, 20) 67 body.quadTo(20.4, 20, 19.5, 18.2) 68 body.lineTo(14.5, 10) 69 body.lineTo(14.5, 4) 70 p.setPen(_pen(c)) 71 p.setBrush(Qt.NoBrush) 72 p.drawPath(body) 73 p.drawLine(QPointF(8, 4), QPointF(16, 4)) 74 liquid = QPainterPath(QPointF(6.8, 14.5)) 75 liquid.lineTo(17.2, 14.5) 76 liquid.lineTo(19.0, 17.6) 77 liquid.quadTo(19.6, 18.8, 18.2, 18.8) 78 liquid.lineTo(5.8, 18.8) 79 liquid.quadTo(4.4, 18.8, 5.0, 17.6) 80 liquid.closeSubpath() 81 p.setPen(Qt.NoPen) 82 p.setBrush(c) 83 p.drawPath(liquid) 84 85 86def _build_tests(p: QPainter, c: QColor): 87 """The hammer, smaller, with a small flask in front of it (bottom right).""" 88 p.save() 89 p.translate(-2, -2) 90 p.scale(0.78, 0.78) 91 _hammer(p, c) 92 p.restore() 93 p.save() 94 p.translate(9.6, 9.4) 95 p.scale(0.62, 0.62) 96 _flask(p, c) 97 p.restore() 98 99 100def _download(p: QPainter, c: QColor): 101 p.setPen(_pen(c, 2)) 102 p.drawLine(QPointF(12, 3.5), QPointF(12, 14)) 103 p.drawPolyline([QPointF(7.5, 9.8), QPointF(12, 14.3), QPointF(16.5, 9.8)]) 104 tray = QPainterPath(QPointF(4, 14.5)) 105 tray.lineTo(4, 18.5) 106 tray.quadTo(4, 20, 5.5, 20) 107 tray.lineTo(18.5, 20) 108 tray.quadTo(20, 20, 20, 18.5) 109 tray.lineTo(20, 14.5) 110 p.setBrush(Qt.NoBrush) 111 p.drawPath(tray) 112 113 114def _broom(p: QPainter, c: QColor): 115 p.setPen(_pen(c, 2)) 116 p.drawLine(QPointF(18.5, 3.5), QPointF(12.6, 11.4)) # handle 117 bristles = QPainterPath(QPointF(10.2, 10.0)) 118 bristles.lineTo(15.0, 13.6) 119 bristles.quadTo(13.8, 18.5, 9.8, 20.5) 120 bristles.lineTo(3.6, 15.9) 121 bristles.quadTo(7.6, 14.6, 10.2, 10.0) 122 bristles.closeSubpath() 123 p.setPen(_pen(c, 1.6)) 124 p.setBrush(Qt.NoBrush) 125 p.drawPath(bristles) 126 p.drawLine(QPointF(7.2, 17.9), QPointF(9.6, 14.6)) 127 p.drawLine(QPointF(10.0, 19.4), QPointF(12.0, 16.4)) 128 129 130def _trash(p: QPainter, c: QColor): 131 p.setPen(_pen(c)) 132 p.setBrush(Qt.NoBrush) 133 p.drawLine(QPointF(4.5, 6.5), QPointF(19.5, 6.5)) 134 p.drawPolyline([QPointF(9.5, 6.5), QPointF(9.5, 4), QPointF(14.5, 4), QPointF(14.5, 6.5)]) 135 can = QPainterPath(QPointF(6.2, 6.5)) 136 can.lineTo(7.2, 18.6) 137 can.quadTo(7.4, 20, 8.8, 20) 138 can.lineTo(15.2, 20) 139 can.quadTo(16.6, 20, 16.8, 18.6) 140 can.lineTo(17.8, 6.5) 141 p.drawPath(can) 142 p.drawLine(QPointF(10.2, 10), QPointF(10.2, 16.5)) 143 p.drawLine(QPointF(13.8, 10), QPointF(13.8, 16.5)) 144 145 146def _bug(p: QPainter, c: QColor): 147 """A beetle: debugging.""" 148 p.setPen(_pen(c)) 149 p.setBrush(Qt.NoBrush) 150 p.drawEllipse(QRectF(6.8, 8.6, 10.4, 12.6)) # body 151 p.drawLine(QPointF(12, 9.2), QPointF(12, 20.8)) # (its wing cases' edge) 152 for side in (-1, 1): # legs and antennae, mirrored 153 for (x1, y1), (x2, y2) in (((7.4, 12.2), (3.6, 10.4)), ((6.9, 15.4), (3.2, 15.4)), 154 ((7.6, 18.6), (4.0, 20.6)), ((10.6, 5.2), (8.8, 2.6))): 155 p.drawLine(QPointF(12 + side * (x1 - 12), y1), QPointF(12 + side * (x2 - 12), y2)) 156 p.setPen(Qt.NoPen) 157 p.setBrush(c) 158 p.drawEllipse(QPointF(12, 7.2), 3.2, 2.6) # head 159 160 161def _refresh(p: QPainter, c: QColor): 162 """A circular arrow: read again.""" 163 p.setPen(_pen(c, 2)) 164 p.setBrush(Qt.NoBrush) 165 arc = QPainterPath() 166 arc.arcMoveTo(QRectF(4.5, 4.5, 15, 15), 70) 167 arc.arcTo(QRectF(4.5, 4.5, 15, 15), 70, 290) 168 p.drawPath(arc) 169 end = arc.currentPosition() # (the circle's right: the head there, pointing on round it, up) 170 p.setPen(Qt.NoPen) 171 p.setBrush(c) 172 p.drawPolygon(QPolygonF([end + QPointF(-4, 0.5), end + QPointF(4, 0.5), end + QPointF(0, -4.5)])) 173 174 175def _unified(p: QPainter, c: QColor): 176 """A diff in one column (not two side by side): lines in one box.""" 177 p.setPen(_pen(c, 1.8)) 178 p.setBrush(Qt.NoBrush) 179 p.drawRoundedRect(QRectF(4, 3.5, 16, 17), 2.5, 2.5) 180 for y in (8, 12, 16): 181 p.drawLine(QPointF(7.5, y), QPointF(16.5 if y != 12 else 13.5, y)) 182 183 184def _palette(p: QPainter, c: QColor): 185 """The Command Palette: a panel with a prompt (">") in its field, a list under it.""" 186 p.setPen(_pen(c, 1.8)) 187 p.setBrush(Qt.NoBrush) 188 p.drawRoundedRect(QRectF(3, 3, 18, 18), 2.5, 2.5) 189 p.drawLine(QPointF(3, 10.5), QPointF(21, 10.5)) # (the field's edge) 190 p.drawPolyline([QPointF(6.2, 5.2), QPointF(8.6, 6.9), QPointF(6.2, 8.6)]) # the prompt 191 p.drawLine(QPointF(10.2, 8.6), QPointF(14, 8.6)) 192 for y, end in ((14, 17.5), (17.5, 14.5)): # the commands listed 193 p.drawLine(QPointF(6.5, y), QPointF(end, y)) 194 195 196def _find_file(p: QPainter, c: QColor): 197 """Go to File: a page (its corner folded) under a magnifying glass.""" 198 p.setPen(_pen(c, 1.7)) 199 p.setBrush(Qt.NoBrush) 200 page = QPainterPath(QPointF(5, 3)) 201 page.lineTo(12.5, 3) 202 page.lineTo(17, 7.5) 203 page.lineTo(17, 10.5) 204 page.moveTo(10, 20.5) 205 page.lineTo(5, 20.5) 206 page.lineTo(5, 3) 207 p.drawPath(page) 208 p.drawPolyline([QPointF(12.5, 3), QPointF(12.5, 7.5), QPointF(17, 7.5)]) # (the fold) 209 p.setPen(_pen(c, 2)) 210 p.drawEllipse(QPointF(15, 15), 3.8, 3.8) # the glass 211 p.drawLine(QPointF(17.8, 17.8), QPointF(21, 21)) # its handle 212 213 214def _memcheck(p: QPainter, c: QColor): 215 """A memory chip under a magnifying glass: Valgrind's memory check.""" 216 p.setPen(_pen(c, 1.6)) 217 p.setBrush(Qt.NoBrush) 218 p.drawRoundedRect(QRectF(3.5, 5.5, 11, 11), 1.5, 1.5) # the chip 219 for i in (8, 11, 14): # its pins 220 p.drawLine(QPointF(i - 1.5, 3), QPointF(i - 1.5, 5.5)) 221 p.drawLine(QPointF(i - 1.5, 16.5), QPointF(i - 1.5, 19)) 222 p.drawLine(QPointF(1, i - 0.5), QPointF(3.5, i - 0.5)) 223 p.setPen(_pen(c, 2)) 224 p.setBrush(QColor(0, 0, 0, 0)) 225 p.drawEllipse(QPointF(15, 15), 4.2, 4.2) # the glass 226 p.drawLine(QPointF(18, 18), QPointF(21.5, 21.5)) # its handle 227 228 229def _pencil(p: QPainter, c: QColor): 230 """Edit.""" 231 p.translate(12, 12) 232 p.rotate(45) 233 p.setPen(_pen(c, 1.8)) 234 p.setBrush(Qt.NoBrush) 235 body = QPainterPath(QPointF(-2.6, -9)) # (from the eraser end down to the point) 236 body.lineTo(2.6, -9) 237 body.lineTo(2.6, 4.5) 238 body.lineTo(0, 9.5) 239 body.lineTo(-2.6, 4.5) 240 body.closeSubpath() 241 p.drawPath(body) 242 p.drawLine(QPointF(-2.6, -5.5), QPointF(2.6, -5.5)) # (the eraser's band) 243 244 245def _save(p: QPainter, c: QColor): 246 """A floppy disk: save.""" 247 p.setPen(_pen(c, 1.8)) 248 p.setBrush(Qt.NoBrush) 249 disk = QPainterPath(QPointF(4.5, 6)) 250 disk.quadTo(4.5, 4.5, 6, 4.5) 251 disk.lineTo(16, 4.5) 252 disk.lineTo(19.5, 8) 253 disk.lineTo(19.5, 18) 254 disk.quadTo(19.5, 19.5, 18, 19.5) 255 disk.lineTo(6, 19.5) 256 disk.quadTo(4.5, 19.5, 4.5, 18) 257 disk.closeSubpath() 258 p.drawPath(disk) 259 p.drawRect(QRectF(8, 4.5, 7, 4.5)) # (the shutter) 260 p.drawRect(QRectF(7.5, 13, 9, 6.5)) # (the label) 261 262 263def _menu(p: QPainter, c: QColor): 264 """Three lines: a menu of more.""" 265 p.setPen(_pen(c, 2)) 266 for y in (7, 12, 17): 267 p.drawLine(QPointF(5, y), QPointF(19, y)) 268 269 270def _pause(p: QPainter, c: QColor): 271 p.setPen(Qt.NoPen) 272 p.setBrush(c) 273 p.drawRoundedRect(QRectF(6.5, 5, 4, 14), 1.2, 1.2) 274 p.drawRoundedRect(QRectF(13.5, 5, 4, 14), 1.2, 1.2) 275 276 277def _continue(p: QPainter, c: QColor): 278 """A bar, then the play triangle: run on from where it's paused.""" 279 p.setPen(Qt.NoPen) 280 p.setBrush(c) 281 p.drawRoundedRect(QRectF(4.5, 5.5, 3, 13), 1, 1) 282 p.translate(2.5, 0) 283 _play(p, c) 284 285 286def _step_dot(p: QPainter, c: QColor): 287 """The line a step lands on, as a dot under its arrow.""" 288 p.setPen(Qt.NoPen) 289 p.setBrush(c) 290 p.drawEllipse(QPointF(12, 19.5), 2.3, 2.3) 291 292 293def _step_over(p: QPainter, c: QColor): 294 """An arrow over the dot: the call run, not gone into.""" 295 p.setPen(_pen(c, 2)) 296 p.setBrush(Qt.NoBrush) 297 arc = QPainterPath(QPointF(4.5, 14)) 298 arc.cubicTo(QPointF(4.5, 3.5), QPointF(19.5, 3.5), QPointF(19.5, 13.5)) 299 p.drawPath(arc) 300 p.drawPolyline([QPointF(16.2, 10.6), QPointF(19.5, 14), QPointF(22.5, 10.6)]) 301 _step_dot(p, c) 302 303 304def _step_into(p: QPainter, c: QColor): 305 """An arrow down to the dot: into the procedure called.""" 306 p.setPen(_pen(c, 2)) 307 p.drawLine(QPointF(12, 3), QPointF(12, 14)) 308 p.drawPolyline([QPointF(8, 10.2), QPointF(12, 14.2), QPointF(16, 10.2)]) 309 _step_dot(p, c) 310 311 312def _step_out(p: QPainter, c: QColor): 313 """An arrow up, away from the dot: out to the caller.""" 314 p.setPen(_pen(c, 2)) 315 p.drawLine(QPointF(12, 3), QPointF(12, 14.5)) 316 p.drawPolyline([QPointF(8, 7), QPointF(12, 3), QPointF(16, 7)]) 317 _step_dot(p, c) 318 319 320def _stop(p: QPainter, c: QColor): 321 p.setPen(Qt.NoPen) 322 p.setBrush(c) 323 p.drawRoundedRect(QRectF(6, 6, 12, 12), 2.5, 2.5) 324 325 326def _robot(p: QPainter, c: QColor): 327 p.setPen(_pen(c, 1.7)) 328 p.setBrush(Qt.NoBrush) 329 # Antenna with a ball on top. 330 p.drawLine(QPointF(12, 4.6), QPointF(12, 7.2)) 331 p.setPen(Qt.NoPen) 332 p.setBrush(c) 333 p.drawEllipse(QPointF(12, 3.4), 1.6, 1.6) 334 # Head. 335 p.setPen(_pen(c, 1.7)) 336 p.setBrush(Qt.NoBrush) 337 p.drawRoundedRect(QRectF(4.5, 7.2, 15, 12.3), 3.6, 3.6) 338 # Ears. 339 p.drawLine(QPointF(2.6, 11.6), QPointF(2.6, 15.0)) 340 p.drawLine(QPointF(21.4, 11.6), QPointF(21.4, 15.0)) 341 # Eyes and mouth. 342 p.setPen(Qt.NoPen) 343 p.setBrush(c) 344 p.drawEllipse(QPointF(9.0, 12.3), 1.7, 1.7) 345 p.drawEllipse(QPointF(15.0, 12.3), 1.7, 1.7) 346 p.setPen(_pen(c, 1.5)) 347 p.drawLine(QPointF(9.6, 16.2), QPointF(14.4, 16.2)) 348 349 350def _format(p: QPainter, c: QColor): 351 # Lines of code, indented as a formatter would. 352 p.setPen(_pen(c, 2.0)) 353 for y, x0, x1 in ((5.5, 3.5, 14), (10, 8, 20.5), (14.5, 8, 17), (19, 3.5, 12)): 354 p.drawLine(QPointF(x0, y), QPointF(x1, y)) 355 # A small sparkle. 356 path = QPainterPath(QPointF(19.5, 14.5)) 357 for x, y in ((20.4, 17.1), (23, 18), (20.4, 18.9), (19.5, 21.5), (18.6, 18.9), (16, 18), (18.6, 17.1)): 358 path.lineTo(x, y) 359 path.closeSubpath() 360 p.setPen(Qt.NoPen) 361 p.setBrush(c) 362 p.drawPath(path) 363 364 365def _more(p: QPainter, c: QColor): 366 # ">>": the toolbar's overflow button. 367 p.setPen(_pen(c, 1.9)) 368 for x in (6.5, 12.5): 369 p.drawPolyline([QPointF(x, 7), QPointF(x + 5, 12), QPointF(x, 17)]) 370 371 372def _close(p: QPainter, c: QColor): 373 # A small "×" (editor tab close buttons). 374 p.setPen(_pen(c, 2.2)) 375 p.drawLine(QPointF(7, 7), QPointF(17, 17)) 376 p.drawLine(QPointF(17, 7), QPointF(7, 17)) 377 378 379def _coverage(p: QPainter, c: QColor): 380 """A gutter of lines, most of them run (filled), one not (outlined).""" 381 p.setPen(_pen(c)) 382 p.setBrush(Qt.NoBrush) 383 p.drawRoundedRect(QRectF(4, 4, 16, 16), 2.5, 2.5) 384 p.setPen(Qt.NoPen) 385 p.setBrush(c) 386 for y, width in ((7, 8), (11, 11), (15, 6)): 387 p.drawRect(QRectF(7, y, width, 2)) 388 p.setPen(_pen(c, 1.4)) 389 p.setBrush(Qt.NoBrush) 390 p.drawRect(QRectF(13.5, 14.5, 3.5, 3)) 391 392 393def _complexity(p: QPainter, c: QColor): 394 """A decision (a diamond) and its two ways.""" 395 p.setPen(_pen(c, 1.8)) 396 p.setBrush(Qt.NoBrush) 397 p.drawPolygon([QPointF(12, 3), QPointF(17, 8), QPointF(12, 13), QPointF(7, 8)]) 398 p.drawLine(QPointF(7, 8), QPointF(5, 8)) 399 p.drawLine(QPointF(5, 8), QPointF(5, 16.5)) 400 p.drawLine(QPointF(17, 8), QPointF(19, 8)) 401 p.drawLine(QPointF(19, 8), QPointF(19, 16.5)) 402 p.drawRoundedRect(QRectF(2.5, 16.5, 5, 4.5), 1.2, 1.2) 403 p.drawRoundedRect(QRectF(16.5, 16.5, 5, 4.5), 1.2, 1.2) 404 405 406def _git(p: QPainter, c: QColor): 407 """A branch: two commits on a line, and one branching off it.""" 408 p.setPen(_pen(c, 1.9)) 409 p.setBrush(Qt.NoBrush) 410 p.drawLine(QPointF(7, 8.6), QPointF(7, 15.4)) 411 path = QPainterPath(QPointF(17, 9.6)) 412 path.cubicTo(QPointF(17, 14), QPointF(7, 12), QPointF(7, 15.4)) 413 p.drawPath(path) 414 for x, y in ((7, 6), (7, 18), (17, 7)): 415 p.drawEllipse(QPointF(x, y), 2.6, 2.6) 416 417 418DRAWERS = { 419 "build": _hammer, 420 "build-tests": _build_tests, 421 "run": _play, 422 "test": _flask, 423 "coverage": _coverage, 424 "git": _git, 425 "complexity": _complexity, 426 "update": _download, 427 "clean": _broom, 428 "clean-all": _trash, 429 "stop": _stop, 430 "pause": _pause, 431 "pencil": _pencil, 432 "memcheck": _memcheck, 433 "palette": _palette, 434 "find-file": _find_file, 435 "save": _save, 436 "menu": _menu, 437 "refresh": _refresh, 438 "unified": _unified, 439 "bug": _bug, 440 "continue": _continue, 441 "step-over": _step_over, 442 "step-into": _step_into, 443 "step-out": _step_out, 444 "robot": _robot, 445 "format": _format, 446 "more": _more, 447 "close": _close, 448} 449 450 451def make_icon(name: str, color: QColor | str) -> QIcon: 452 """An icon by name (see ``DRAWERS``) in the given color; Qt derives the 453 greyed-out disabled look from it.""" 454 color = QColor(color) 455 icon = QIcon() 456 for ratio in (1, 2, 3): 457 pixmap, painter = _canvas(ratio) 458 DRAWERS[name](painter, color) 459 painter.end() 460 icon.addPixmap(pixmap) 461 return icon 462 463 464# ------------------------------------------------------------ folder badges 465def _badge_code(p: QPainter, c: QColor): 466 p.setPen(_pen(c, 2.6)) 467 p.drawPolyline([QPointF(8.5, 7), QPointF(4, 12), QPointF(8.5, 17)]) 468 p.drawPolyline([QPointF(15.5, 7), QPointF(20, 12), QPointF(15.5, 17)]) 469 p.drawLine(QPointF(13.6, 5.5), QPointF(10.4, 18.5)) 470 471 472def _badge_bulb(p: QPainter, c: QColor): 473 p.setPen(Qt.NoPen) 474 p.setBrush(c) 475 p.drawEllipse(QPointF(12, 9.5), 6, 6) 476 p.drawRoundedRect(QRectF(9, 14, 6, 4.5), 1, 1) 477 p.drawRoundedRect(QRectF(9.8, 19.2, 4.4, 2), 1, 1) 478 479 480def _badge_hash(p: QPainter, c: QColor): 481 p.setPen(_pen(c, 2.4)) 482 p.drawLine(QPointF(9.5, 4.5), QPointF(8, 19.5)) 483 p.drawLine(QPointF(16, 4.5), QPointF(14.5, 19.5)) 484 p.drawLine(QPointF(5, 9.5), QPointF(19.5, 9.5)) 485 p.drawLine(QPointF(4.5, 14.5), QPointF(19, 14.5)) 486 487 488def _badge_cube(p: QPainter, c: QColor): 489 p.setPen(_pen(c, 2.0)) 490 p.setBrush(Qt.NoBrush) 491 top = [QPointF(12, 3.5), QPointF(20, 7.5), QPointF(12, 11.5), QPointF(4, 7.5), QPointF(12, 3.5)] 492 p.drawPolyline(top) 493 p.drawPolyline([QPointF(4, 7.5), QPointF(4, 16.5), QPointF(12, 20.5), QPointF(20, 16.5), 494 QPointF(20, 7.5)]) 495 p.drawLine(QPointF(12, 11.5), QPointF(12, 20.5)) 496 497 498BADGES = { 499 "code": _badge_code, 500 "run": _play, 501 "test": _flask, 502 "example": _badge_bulb, 503 "include": _badge_hash, 504 "manifest": _badge_cube, 505} 506# Badge background colors by fpm folder role. 507BADGE_COLORS = { 508 "code": "#1c7ed6", "run": "#2f9e44", "test": "#7048e8", "example": "#e8590c", 509 "include": "#868e96", "manifest": "#e8590c", 510} 511 512 513def badged_icon(base: QIcon, badge: str, sizes=(16, 20, 32)) -> QIcon: 514 """``base`` (e.g. the system folder icon) with a small colored badge in 515 its lower-right corner showing ``badge`` (see ``BADGES``).""" 516 color = QColor(BADGE_COLORS.get(badge, "#868e96")) 517 icon = QIcon() 518 for size in sizes: 519 for ratio in (1, 2): 520 pixmap = QPixmap(int(size * ratio), int(size * ratio)) 521 pixmap.setDevicePixelRatio(ratio) 522 pixmap.fill(Qt.transparent) 523 painter = QPainter(pixmap) 524 painter.setRenderHint(QPainter.Antialiasing) 525 base.paint(painter, 0, 0, size, size) 526 d = size * 0.62 # badge diameter 527 x, y = size - d, size - d 528 painter.setPen(QPen(QColor(255, 255, 255, 230), max(1.0, size / 16))) 529 painter.setBrush(color) 530 painter.drawEllipse(QRectF(x, y, d, d)) 531 painter.save() 532 painter.translate(x + d * 0.18, y + d * 0.18) 533 painter.scale(d * 0.64 / SIZE, d * 0.64 / SIZE) 534 BADGES[badge](painter, QColor("white")) 535 painter.restore() 536 painter.end() 537 icon.addPixmap(pixmap) 538 return icon 539 540 541 542# ----------------------------------------------------------------- app icon 543LOGO_PATH = Path(__file__).with_name("resources") / "fpm-logo-color.svg" 544FORTRAN_FILE_PATH = Path(__file__).with_name("resources") / "fortran-file.svg" 545# Fortran sources (any case, e.g. .F90): shown with the Fortran file icon. 546FORTRAN_SUFFIXES = {".f", ".for", ".ftn", ".f77", ".f90", ".f95", ".f03", ".f08", ".f18", ".f23", 547 ".fpp", ".fypp"} 548# Fortran include files: the same icon in gray. 549FORTRAN_INCLUDE_SUFFIXES = {".inc", ".if"} 550_fortran_icons: dict[bool, QIcon] = {} 551 552 553def fortran_file_icon(include: bool = False) -> QIcon: 554 """The icon for Fortran source files (a page with the Fortran "F"), or 555 in gray for include files, drawn crisply at each size.""" 556 icon = _fortran_icons.get(include) 557 if icon is None: 558 from qtpy.QtCore import QByteArray 559 from qtpy.QtSvg import QSvgRenderer 560 561 svg = FORTRAN_FILE_PATH.read_text(encoding="utf-8") 562 if include: 563 svg = svg.replace("#734f96", "#6c757d").replace("#a98cc9", "#adb5bd") 564 renderer = QSvgRenderer(QByteArray(svg.encode())) 565 icon = _fortran_icons[include] = QIcon() 566 if renderer.isValid(): 567 for size in (16, 20, 24, 32, 40, 48, 64): 568 pixmap = QPixmap(size, size) 569 pixmap.fill(Qt.transparent) 570 painter = QPainter(pixmap) 571 painter.setRenderHint(QPainter.Antialiasing) 572 renderer.render(painter) 573 painter.end() 574 icon.addPixmap(pixmap) 575 return icon 576 577 578def app_icon() -> QIcon: 579 """The application icon: the fpm logo centred on a white rounded square 580 (the usual macOS app-icon shape), drawn crisply at each size.""" 581 from qtpy.QtSvg import QSvgRenderer 582 583 renderer = QSvgRenderer(str(LOGO_PATH)) 584 icon = QIcon() 585 if not renderer.isValid(): 586 return icon 587 for size in (16, 32, 48, 64, 128, 256, 512, 1024): 588 pixmap = QPixmap(size, size) 589 pixmap.fill(Qt.transparent) 590 p = QPainter(pixmap) 591 draw_app_icon(p, size, renderer) 592 p.end() 593 icon.addPixmap(pixmap) 594 return icon 595 596 597def draw_app_icon(p: QPainter, size: float, renderer): 598 """Paint the app icon (`app_icon`) at (0, 0), ``size`` across, with the 599 logo from ``renderer`` (a QSvgRenderer).""" 600 p.setRenderHint(QPainter.Antialiasing) 601 p.setRenderHint(QPainter.SmoothPixmapTransform) 602 view = renderer.viewBoxF() 603 aspect = view.width() / view.height() if view.height() else 1.0 604 # macOS icons leave ~10% margin around the rounded square. 605 margin = size * 0.09 606 tile = QRectF(margin, margin, size - 2 * margin, size - 2 * margin) 607 radius = tile.width() * 0.225 608 if size >= 32: # a soft shadow (too muddy at tiny sizes) 609 for i, alpha in enumerate((18, 12, 6)): 610 grow = size * 0.006 * (i + 1) 611 p.setPen(Qt.NoPen) 612 p.setBrush(QColor(0, 0, 0, alpha)) 613 p.drawRoundedRect(tile.adjusted(-grow, -grow + size * 0.012, grow, grow + size * 0.012), 614 radius + grow, radius + grow) 615 gradient = QLinearGradient(tile.topLeft(), tile.bottomLeft()) 616 gradient.setColorAt(0, QColor("#ffffff")) 617 gradient.setColorAt(1, QColor("#eef0f6")) 618 p.setBrush(gradient) 619 p.setPen(QPen(QColor(0, 0, 0, 28), max(1.0, size / 256))) 620 p.drawRoundedRect(tile, radius, radius) 621 # The logo, as large as fits with some breathing room. 622 box = tile.adjusted(tile.width() * 0.16, tile.height() * 0.12, 623 -tile.width() * 0.16, -tile.height() * 0.12) 624 h = box.height() 625 w = h * aspect 626 if w > box.width(): 627 w = box.width() 628 h = w / aspect 629 renderer.render(p, QRectF(box.center().x() - w / 2, box.center().y() - h / 2, w, h)) 630 631 632@cache 633def _logo_text() -> str: 634 return LOGO_PATH.read_text() 635 636 637def recolored_logo(turn: float) -> bytes: 638 """The fpm logo's SVG with each piece's color moved ``turn`` of the way 639 around the color wheel (0 to 1), and a little more or less for each piece 640 (so the colors ripple as ``turn`` grows), keeping their depth. At 0 (or 641 1) it's the logo's own colors.""" 642 svg = _logo_text() 643 pieces = iter(range(100)) 644 645 def shift(match: re.Match) -> str: 646 color = QColor(match.group(1)) 647 h, s, v, _ = color.getHsvF() 648 phase = 2 * math.pi * next(pieces) / 5 649 ripple = 0.04 * (math.sin(4 * math.pi * turn + phase) - math.sin(phase)) # (0 at turn 0 and 1) 650 color.setHsvF((max(h, 0) + turn + ripple) % 1.0, s, v) 651 return f"fill:{color.name()}" 652 653 return re.sub(r"fill:(#[0-9a-fA-F]{6})", shift, svg).encode() 654 655 656# ------------------------------------------------------------ symbol kinds 657# Outline icons: a colored badge with a letter for each kind of Fortran 658# symbol (like the symbol icons of other editors). 659SYMBOL_BADGES = { 660 "module": ("M", "#8e44ad"), 661 "submodule": ("M", "#b15dcc"), 662 "program": ("P", "#5f3dc4"), 663 "subroutine": ("S", "#1c7ed6"), 664 "function": ("F", "#e8590c"), 665 "type": ("T", "#0ca678"), 666 "interface": ("I", "#2f9e44"), 667 "enum": ("E", "#f08c00"), 668 "constructor": ("C", "#c2255c"), 669} 670SYMBOL_INDENT = 12 # logical pixels per nesting level in lists of symbols 671_symbol_icons: dict = {} 672 673 674def symbol_icon(kind: str) -> QIcon: 675 """A 16 px badge for a kind of symbol ("function", "module"…).""" 676 if kind in _symbol_icons: 677 return _symbol_icons[kind] 678 letter, color = SYMBOL_BADGES.get(kind, ((kind[:1] or "?").upper(), "#868e96")) 679 width, height = 16, 16 680 icon = QIcon() 681 for ratio in (1, 2, 3): 682 pixmap = QPixmap(int(width * ratio), int(height * ratio)) 683 pixmap.setDevicePixelRatio(ratio) 684 pixmap.fill(Qt.transparent) 685 p = QPainter(pixmap) 686 p.setRenderHint(QPainter.Antialiasing) 687 p.setRenderHint(QPainter.TextAntialiasing) 688 box = QRectF(1, 1, 14, 14) 689 p.setPen(Qt.NoPen) 690 p.setBrush(QColor(color)) 691 p.drawRoundedRect(box, 3.5, 3.5) 692 font = p.font() 693 font.setPixelSize(10) 694 font.setBold(True) 695 p.setFont(font) 696 p.setPen(QColor("white")) 697 p.drawText(box, Qt.AlignCenter, letter) 698 p.end() 699 icon.addPixmap(pixmap) 700 _symbol_icons[kind] = icon 701 return icon
452def make_icon(name: str, color: QColor | str) -> QIcon: 453 """An icon by name (see ``DRAWERS``) in the given color; Qt derives the 454 greyed-out disabled look from it.""" 455 color = QColor(color) 456 icon = QIcon() 457 for ratio in (1, 2, 3): 458 pixmap, painter = _canvas(ratio) 459 DRAWERS[name](painter, color) 460 painter.end() 461 icon.addPixmap(pixmap) 462 return icon
An icon by name (see DRAWERS) in the given color; Qt derives the
greyed-out disabled look from it.
514def badged_icon(base: QIcon, badge: str, sizes=(16, 20, 32)) -> QIcon: 515 """``base`` (e.g. the system folder icon) with a small colored badge in 516 its lower-right corner showing ``badge`` (see ``BADGES``).""" 517 color = QColor(BADGE_COLORS.get(badge, "#868e96")) 518 icon = QIcon() 519 for size in sizes: 520 for ratio in (1, 2): 521 pixmap = QPixmap(int(size * ratio), int(size * ratio)) 522 pixmap.setDevicePixelRatio(ratio) 523 pixmap.fill(Qt.transparent) 524 painter = QPainter(pixmap) 525 painter.setRenderHint(QPainter.Antialiasing) 526 base.paint(painter, 0, 0, size, size) 527 d = size * 0.62 # badge diameter 528 x, y = size - d, size - d 529 painter.setPen(QPen(QColor(255, 255, 255, 230), max(1.0, size / 16))) 530 painter.setBrush(color) 531 painter.drawEllipse(QRectF(x, y, d, d)) 532 painter.save() 533 painter.translate(x + d * 0.18, y + d * 0.18) 534 painter.scale(d * 0.64 / SIZE, d * 0.64 / SIZE) 535 BADGES[badge](painter, QColor("white")) 536 painter.restore() 537 painter.end() 538 icon.addPixmap(pixmap) 539 return icon
base (e.g. the system folder icon) with a small colored badge in
its lower-right corner showing badge (see BADGES).
554def fortran_file_icon(include: bool = False) -> QIcon: 555 """The icon for Fortran source files (a page with the Fortran "F"), or 556 in gray for include files, drawn crisply at each size.""" 557 icon = _fortran_icons.get(include) 558 if icon is None: 559 from qtpy.QtCore import QByteArray 560 from qtpy.QtSvg import QSvgRenderer 561 562 svg = FORTRAN_FILE_PATH.read_text(encoding="utf-8") 563 if include: 564 svg = svg.replace("#734f96", "#6c757d").replace("#a98cc9", "#adb5bd") 565 renderer = QSvgRenderer(QByteArray(svg.encode())) 566 icon = _fortran_icons[include] = QIcon() 567 if renderer.isValid(): 568 for size in (16, 20, 24, 32, 40, 48, 64): 569 pixmap = QPixmap(size, size) 570 pixmap.fill(Qt.transparent) 571 painter = QPainter(pixmap) 572 painter.setRenderHint(QPainter.Antialiasing) 573 renderer.render(painter) 574 painter.end() 575 icon.addPixmap(pixmap) 576 return icon
The icon for Fortran source files (a page with the Fortran "F"), or in gray for include files, drawn crisply at each size.
579def app_icon() -> QIcon: 580 """The application icon: the fpm logo centred on a white rounded square 581 (the usual macOS app-icon shape), drawn crisply at each size.""" 582 from qtpy.QtSvg import QSvgRenderer 583 584 renderer = QSvgRenderer(str(LOGO_PATH)) 585 icon = QIcon() 586 if not renderer.isValid(): 587 return icon 588 for size in (16, 32, 48, 64, 128, 256, 512, 1024): 589 pixmap = QPixmap(size, size) 590 pixmap.fill(Qt.transparent) 591 p = QPainter(pixmap) 592 draw_app_icon(p, size, renderer) 593 p.end() 594 icon.addPixmap(pixmap) 595 return icon
The application icon: the fpm logo centred on a white rounded square (the usual macOS app-icon shape), drawn crisply at each size.
598def draw_app_icon(p: QPainter, size: float, renderer): 599 """Paint the app icon (`app_icon`) at (0, 0), ``size`` across, with the 600 logo from ``renderer`` (a QSvgRenderer).""" 601 p.setRenderHint(QPainter.Antialiasing) 602 p.setRenderHint(QPainter.SmoothPixmapTransform) 603 view = renderer.viewBoxF() 604 aspect = view.width() / view.height() if view.height() else 1.0 605 # macOS icons leave ~10% margin around the rounded square. 606 margin = size * 0.09 607 tile = QRectF(margin, margin, size - 2 * margin, size - 2 * margin) 608 radius = tile.width() * 0.225 609 if size >= 32: # a soft shadow (too muddy at tiny sizes) 610 for i, alpha in enumerate((18, 12, 6)): 611 grow = size * 0.006 * (i + 1) 612 p.setPen(Qt.NoPen) 613 p.setBrush(QColor(0, 0, 0, alpha)) 614 p.drawRoundedRect(tile.adjusted(-grow, -grow + size * 0.012, grow, grow + size * 0.012), 615 radius + grow, radius + grow) 616 gradient = QLinearGradient(tile.topLeft(), tile.bottomLeft()) 617 gradient.setColorAt(0, QColor("#ffffff")) 618 gradient.setColorAt(1, QColor("#eef0f6")) 619 p.setBrush(gradient) 620 p.setPen(QPen(QColor(0, 0, 0, 28), max(1.0, size / 256))) 621 p.drawRoundedRect(tile, radius, radius) 622 # The logo, as large as fits with some breathing room. 623 box = tile.adjusted(tile.width() * 0.16, tile.height() * 0.12, 624 -tile.width() * 0.16, -tile.height() * 0.12) 625 h = box.height() 626 w = h * aspect 627 if w > box.width(): 628 w = box.width() 629 h = w / aspect 630 renderer.render(p, QRectF(box.center().x() - w / 2, box.center().y() - h / 2, w, h))
Paint the app icon (app_icon) at (0, 0), size across, with the
logo from renderer (a QSvgRenderer).
638def recolored_logo(turn: float) -> bytes: 639 """The fpm logo's SVG with each piece's color moved ``turn`` of the way 640 around the color wheel (0 to 1), and a little more or less for each piece 641 (so the colors ripple as ``turn`` grows), keeping their depth. At 0 (or 642 1) it's the logo's own colors.""" 643 svg = _logo_text() 644 pieces = iter(range(100)) 645 646 def shift(match: re.Match) -> str: 647 color = QColor(match.group(1)) 648 h, s, v, _ = color.getHsvF() 649 phase = 2 * math.pi * next(pieces) / 5 650 ripple = 0.04 * (math.sin(4 * math.pi * turn + phase) - math.sin(phase)) # (0 at turn 0 and 1) 651 color.setHsvF((max(h, 0) + turn + ripple) % 1.0, s, v) 652 return f"fill:{color.name()}" 653 654 return re.sub(r"fill:(#[0-9a-fA-F]{6})", shift, svg).encode()
The fpm logo's SVG with each piece's color moved turn of the way
around the color wheel (0 to 1), and a little more or less for each piece
(so the colors ripple as turn grows), keeping their depth. At 0 (or
1) it's the logo's own colors.
675def symbol_icon(kind: str) -> QIcon: 676 """A 16 px badge for a kind of symbol ("function", "module"…).""" 677 if kind in _symbol_icons: 678 return _symbol_icons[kind] 679 letter, color = SYMBOL_BADGES.get(kind, ((kind[:1] or "?").upper(), "#868e96")) 680 width, height = 16, 16 681 icon = QIcon() 682 for ratio in (1, 2, 3): 683 pixmap = QPixmap(int(width * ratio), int(height * ratio)) 684 pixmap.setDevicePixelRatio(ratio) 685 pixmap.fill(Qt.transparent) 686 p = QPainter(pixmap) 687 p.setRenderHint(QPainter.Antialiasing) 688 p.setRenderHint(QPainter.TextAntialiasing) 689 box = QRectF(1, 1, 14, 14) 690 p.setPen(Qt.NoPen) 691 p.setBrush(QColor(color)) 692 p.drawRoundedRect(box, 3.5, 3.5) 693 font = p.font() 694 font.setPixelSize(10) 695 font.setBold(True) 696 p.setFont(font) 697 p.setPen(QColor("white")) 698 p.drawText(box, Qt.AlignCenter, letter) 699 p.end() 700 icon.addPixmap(pixmap) 701 _symbol_icons[kind] = icon 702 return icon
A 16 px badge for a kind of symbol ("function", "module"…).