Coverage for vibefpmgui/icons.py: 99%

479 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-01 20:51 +0000

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(): 566 ↛ 575line 566 didn't jump to line 575 because the condition on line 566 was always true

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(): 585 ↛ 586line 585 didn't jump to line 586 because the condition on line 585 was never true

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(): 626 ↛ 627line 626 didn't jump to line 627 because the condition on line 626 was never true

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