Skip to content

Commit a0eb2e3

Browse files
subkoksclaude
andauthored
feat(gui): sidebar nav icons matching the app icon (#19)
## Summary Give the sidebar nav items (Download · Organize · Settings) icons drawn in the **same visual language as the app icon** — a rounded-square "chip" silhouette in Apple-blue with rounded strokes. - New `gui/ui/icons.py`: programmatic QPainter glyphs (down-arrow + tray, 2×2 nodes, sliders), rendered at 2× for HiDPI. Accent blue is identical in both palettes, so they match the app icon and look correct on dark **and** light themes with no per-theme recolor. - Wired into `main_window` sidebar; also set the window/dock icon from `app.icns`. ## Verification - `ruff check src/gui` clean; dev launch warning-free. - Rendered both-theme preview confirms legibility and app-icon consistency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0eda08c commit a0eb2e3

3 files changed

Lines changed: 115 additions & 3 deletions

File tree

src/gui/app.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@
2121
sys.path.insert(0, str(_SRC))
2222

2323
import qasync # noqa: E402
24-
from PySide6.QtGui import QFontDatabase # noqa: E402
24+
from PySide6.QtGui import QFontDatabase, QIcon # noqa: E402
2525
from PySide6.QtWidgets import QApplication # noqa: E402
2626

2727
from gui.core import paths # noqa: E402
2828
from gui.core.settings import APP_NAME, Settings # noqa: E402
2929
from gui.ui.main_window import MainWindow # noqa: E402
3030
from gui.ui.theme import ThemeManager # noqa: E402
3131

32+
_ICON_FILE = Path(__file__).resolve().parent / "resources" / "app.icns"
33+
3234

3335
def main() -> None:
3436
app = QApplication(sys.argv)
@@ -38,6 +40,8 @@ def main() -> None:
3840

3941
# Use the real OS UI font (avoids a missing -apple-system family lookup).
4042
app.setFont(QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont))
43+
if _ICON_FILE.exists():
44+
app.setWindowIcon(QIcon(str(_ICON_FILE)))
4145

4246
settings = Settings.load()
4347
paths.apply(settings)

src/gui/ui/icons.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Programmatic sidebar icons drawn in the app-icon language.
2+
3+
Each glyph sits inside the same rounded-square "chip" silhouette as the app icon, in
4+
Apple-blue, with rounded strokes and small pad dots echoing the icon's traces. Rendered
5+
at 2x for crisp HiDPI. Accent blue is identical in both themes, so icons match the app
6+
icon and need no per-theme recolor.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from collections.abc import Callable
12+
13+
from PySide6.QtCore import QPointF, QRectF, Qt
14+
from PySide6.QtGui import QColor, QIcon, QPainter, QPen, QPixmap
15+
16+
ACCENT = "#2f6df6"
17+
_SCALE = 2
18+
19+
20+
def _pixmap(size: int, draw: Callable[[QPainter, float, QColor], None], color: str) -> QPixmap:
21+
pm = QPixmap(size * _SCALE, size * _SCALE)
22+
pm.fill(Qt.GlobalColor.transparent)
23+
p = QPainter(pm)
24+
p.setRenderHint(QPainter.RenderHint.Antialiasing, True)
25+
qcolor = QColor(color)
26+
pen = QPen(qcolor, 1.7 * _SCALE)
27+
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
28+
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
29+
p.setPen(pen)
30+
draw(p, size * _SCALE, qcolor)
31+
p.end()
32+
pm.setDevicePixelRatio(_SCALE)
33+
return pm
34+
35+
36+
def _chip(p: QPainter, s: float) -> QRectF:
37+
"""The shared rounded-square silhouette + two pad dots, like the app icon."""
38+
m = s * 0.10
39+
rect = QRectF(m, m, s - 2 * m, s - 2 * m)
40+
p.setBrush(Qt.BrushStyle.NoBrush)
41+
p.drawRoundedRect(rect, s * 0.24, s * 0.24)
42+
return rect
43+
44+
45+
def _draw_download(p: QPainter, s: float, c: QColor) -> None:
46+
r = _chip(p, s)
47+
cx = r.center().x()
48+
top = r.top() + r.height() * 0.22
49+
mid = r.top() + r.height() * 0.52
50+
# shaft
51+
p.drawLine(QPointF(cx, top), QPointF(cx, mid))
52+
# arrow head
53+
a = r.width() * 0.16
54+
p.drawLine(QPointF(cx - a, mid - a), QPointF(cx, mid))
55+
p.drawLine(QPointF(cx + a, mid - a), QPointF(cx, mid))
56+
# tray
57+
by = r.bottom() - r.height() * 0.22
58+
p.drawLine(QPointF(r.left() + r.width() * 0.28, by), QPointF(r.right() - r.width() * 0.28, by))
59+
60+
61+
def _draw_organize(p: QPainter, s: float, c: QColor) -> None:
62+
r = _chip(p, s)
63+
p.setBrush(c)
64+
dot = r.width() * 0.075
65+
xs = (r.left() + r.width() * 0.36, r.left() + r.width() * 0.64)
66+
ys = (r.top() + r.height() * 0.36, r.top() + r.height() * 0.64)
67+
for x in xs:
68+
for y in ys:
69+
p.drawEllipse(QPointF(x, y), dot, dot)
70+
p.setBrush(Qt.BrushStyle.NoBrush)
71+
72+
73+
def _draw_settings(p: QPainter, s: float, c: QColor) -> None:
74+
r = _chip(p, s)
75+
knob = r.width() * 0.075
76+
rows = (0.34, 0.5, 0.66)
77+
knob_x = (0.62, 0.40, 0.58)
78+
for row, kx in zip(rows, knob_x, strict=True):
79+
y = r.top() + r.height() * row
80+
p.setBrush(Qt.BrushStyle.NoBrush)
81+
p.drawLine(
82+
QPointF(r.left() + r.width() * 0.26, y), QPointF(r.right() - r.width() * 0.26, y)
83+
)
84+
p.setBrush(c)
85+
p.drawEllipse(QPointF(r.left() + r.width() * kx, y), knob, knob)
86+
p.setBrush(Qt.BrushStyle.NoBrush)
87+
88+
89+
_GLYPHS: dict[str, Callable[[QPainter, float, QColor], None]] = {
90+
"download": _draw_download,
91+
"organize": _draw_organize,
92+
"settings": _draw_settings,
93+
}
94+
95+
96+
def nav_icon(name: str, size: int = 18, color: str = ACCENT) -> QIcon:
97+
draw = _GLYPHS.get(name)
98+
if draw is None:
99+
return QIcon()
100+
return QIcon(_pixmap(size, draw, color))

src/gui/ui/main_window.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import asyncio
66

7-
from PySide6.QtCore import QUrl
7+
from PySide6.QtCore import QSize, QUrl
88
from PySide6.QtGui import QDesktopServices
99
from PySide6.QtWidgets import (
1010
QButtonGroup,
@@ -24,6 +24,7 @@
2424
from ..core.config import get_credentials, load_config
2525
from ..core.settings import Settings
2626
from .download_view import DownloadView
27+
from .icons import nav_icon
2728
from .login_dialog import LoginCancelled, LoginDialog
2829
from .organize_view import OrganizeView
2930
from .settings_dialog import SettingsDialog
@@ -78,13 +79,18 @@ def _build_sidebar(self) -> QWidget:
7879
layout.addWidget(subtitle)
7980
layout.addSpacing(10)
8081

82+
nav_icon_size = QSize(18, 18)
8183
self._nav_group = QButtonGroup(self)
8284
self._nav_group.setExclusive(True)
83-
for index, label in enumerate(("Download", "Organize")):
85+
for index, (label, glyph) in enumerate(
86+
(("Download", "download"), ("Organize", "organize"))
87+
):
8488
btn = QPushButton(label)
8589
btn.setObjectName("NavButton")
8690
btn.setCheckable(True)
8791
btn.setChecked(index == 0)
92+
btn.setIcon(nav_icon(glyph))
93+
btn.setIconSize(nav_icon_size)
8894
btn.clicked.connect(lambda _=False, i=index: self._stack.setCurrentIndex(i))
8995
self._nav_group.addButton(btn, index)
9096
layout.addWidget(btn)
@@ -93,6 +99,8 @@ def _build_sidebar(self) -> QWidget:
9399

94100
settings_btn = QPushButton("Settings")
95101
settings_btn.setObjectName("NavButton")
102+
settings_btn.setIcon(nav_icon("settings"))
103+
settings_btn.setIconSize(nav_icon_size)
96104
settings_btn.clicked.connect(self._open_settings)
97105
layout.addWidget(settings_btn)
98106
return bar

0 commit comments

Comments
 (0)