Skip to content

feat(macos): self-drawn window chrome + app-wide antialiased rounded UI#5

Open
Miint-Sunny wants to merge 20 commits into
1756141021:masterfrom
Miint-Sunny:feat/macos-window-chrome
Open

feat(macos): self-drawn window chrome + app-wide antialiased rounded UI#5
Miint-Sunny wants to merge 20 commits into
1756141021:masterfrom
Miint-Sunny:feat/macos-window-chrome

Conversation

@Miint-Sunny

Copy link
Copy Markdown
Contributor

概要

macOS 集成第三轮:自绘窗口镶边 + 全应用抗锯齿圆角语言。

Qt 的样式表引擎光栅化 border-radius不做抗锯齿,整个 UI 的圆角都有阶梯锯齿。本 PR 把所有可见圆角表面迁移到自绘/预渲染抗锯齿路径,并统一了设计语言。

主要内容

  • macOS 窗口镶边:Qt 6.9 ExpandedClientAreaHint 扩展客户区,内容延伸到原生标题栏下,消除"双标题栏";HAINTAG_MAC_CLASSIC=1 回退经典模式。窗口壳(surface/标题条/内容区)自绘 AA 圆角,Windows/Linux 无边框路径同样受益
  • 抗锯齿基建:paint_rounded_surface/rounded_rect_path(QPainter 自绘)、qss_surfaces.py(9-patch 预渲染,供 QSS 控件保留 :hover/:focus 状态机)、to_qcolor(解析调色板的 CSS rgba() 字符串——直接传给 QColor() 会静默画成纯黑)
  • 自绘控件族(widgets/common.py):HoverPillButton(填充式圆角方按钮,全应用按钮统一形状)、DashedRectButton/DashedCircleButton(虚线框)、RoundHandleSlider(AA 圆头滑杆)、RoundedPanelPillLabel
  • 覆盖范围:卡片/标题栏/dock/设置面板/弹窗族/图库/OC 气泡/图像反推/时间线/历史侧栏/低频对话框(更新器、崩溃报告、销毁模板编辑器、新手引导)——清点自动化审计的 126 处圆角表面全部处理
  • 统一图钉图标:📌 emoji → 矢量图钉(未钉描边/已钉填充,4x 过采样)
  • 修复:浮动卡片 dock 判定、9-patch 下 macOS combo 弹出列表被裁(改用 Fusion 基底的标准下拉)

验证

  • 本地 ruff check + pytest(137 例)全绿
  • macOS 真机逐控件像素级截图验证;Windows 路径经 CI 冒烟

🤖 Generated with Claude Code

Miint-Sunny and others added 20 commits July 2, 2026 10:43
macOS previously kept the stock native title bar (Plan B), which sat above
the app's own toolbar and produced a visible "double bar". Qt 6.9's
ExpandedClientAreaHint + NoTitleBarBackgroundHint extend the content under
the native title bar while keeping the real traffic lights, collapsing the
two bars into one. WindowSurface fills the window edge-to-edge (margin 0)
because the native window mask already rounds the corners; the 8px inset
band is only needed for the Windows frameless shadow/resize zone.

The earlier blocker (QTBUG-133215, dark bar + traffic lights floating over
content) is fixed in the bundled Qt 6.11. HAINTAG_MAC_CLASSIC=1 restores the
stock native title bar as a fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_on_card_finished decided docking with per-edge half-plane tests in GLOBAL
coordinates for the left/right/top/bottom docks (e.g. "card.left() <
dock.right() + 30"). For a card floated out into its own window that meant
releasing it anywhere in that half of the screen — or on an adjacent
display — vacuumed it into the dock, so it appeared to vanish. Replace the
four half-plane branches with the same "intersects the expanded dock rect"
test the floating dock already used, so a card only docks when actually
dropped onto the dock band.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… corners

Corner radii were ad hoc: ~178 border-radius sites across 25 files using 9
distinct values (0/2/3/4/6/7/8/10/12 px), some DPI-scaled and some not, with
the workbench card at 4px vs generic cards at 10px.

Introduce one scale — rad_none/xs/sm/md (0/2/6/10) — exposed as QSS palette
tokens ({rad_*}, mirroring the existing fs_* pipeline) plus RAD_* constants
and a _rad() helper in ui_tokens for inline stylesheets. Every card, panel,
dialog, button, input and menu now resolves to one of those four values, so
retuning the whole app is a one-line change. Pills, circular slider knobs,
the toggle track and deliberate square segmented joins are preserved.

Also fix the blurry card corners: #WidgetDragStrip painted its own rounded
top corners at the same radius directly over #WidgetCard's, so two
independently-antialiased semi-transparent edges blended into a doubled/soft
corner. The strip is now a plain rectangle inside the card (single rounded
edge), and the card border goes from {line} (0.08 alpha) to {line_strong}
(0.18) so that edge reads crisp against the dark workspace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pin / always-on-top toggles used the 📌 / 📍 emoji, which renders as an
OS-specific colour bitmap that clashes with the flat monochrome chrome and
cannot follow the palette. Add native_app/icons.py — a QPainter push-pin drawn
in an explicit colour (upright when pinned, tilted off-vertical when not) — and
switch the title-bar, widget-card and image-manager pin buttons from setText()
to setIcon(), recoloured on toggle and on theme change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QSS border-radius corners are not antialiased in Qt's stylesheet engine,
so every rounded control/surface shows stair-stepped corners. Add the
shared machinery to paint them crisply instead:

- icons.py: to_qcolor() parses CSS rgba() palette strings (QColor() can't
  and silently yields black); rounded_rect_path()/paint_rounded_surface()
  are the QPainter replacement for QSS border-radius (fill/border/dashed);
  pin glyph refined (round head, outline/fill states, 4x oversample).
- qss_surfaces.py (new): pre-renders antialiased rounded fills to cached
  1x/@2x PNGs applied as a 9-patch border-image, so QSS-styled controls
  (buttons/inputs/menus) get crisp corners while keeping state selectors.
- common.py: HoverPillButton, DashedCircleButton, DashedRectButton —
  self-painted button surfaces for titlebar pills and dashed add/ghost pads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace QSS border-radius (aliased) across containers and controls:

- Cards/panels/popups (DockPanel, SettingsPanel, floating tray, tag
  completer, image manager surfaces) paint their fill+border in paintEvent
  with WA_TranslucentBackground; theme.py drops their QSS border/radius.
- theme.control_surfaces_qss() appended after scale_qss() re-skins buttons,
  the input family and menus with the 9-patch AA surfaces; main.py wires it.
- image_manager QPainter paths route palette values through to_qcolor()
  (they were passing rgba() strings straight to QColor(), painting black).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the main window's controls onto the antialiased primitives and strip
their now-redundant QSS border-radius:

- Widget cards self-paint body/header/border; grip, pin, close and resize
  handles become HoverPillButtons (titlebar pills, main + normal chrome).
- Send/summary buttons (input), OC chip (workbench) use pill surfaces.
- Dashed controls self-paint via DashedRectButton / _DropZone.set_dashed():
  ghost add/preview (prompt manager), example image-select pad, local & LLM
  drop zones (interrogator), library ref-image add, LLM thumb add.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QSS slider handles use border-radius==half-width to stay circular, which
Qt rasterises with visible stair-stepping — most obvious of all the aliased
controls since the eye reads a circle's jagged edge instantly. Add
RoundHandleSlider: the groove/sub-page stay QSS (2px, no visible aliasing)
while the knob is drawn as an antialiased circle in paintEvent (handle kept
transparent in QSS but sized so subControlRect still lays it out). All five
sliders (settings temp/top-p/penalties, interrogator thresholds, image
manager thumb size, opacity popup) unify on one accent_handle knob.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hover and active fills on titlebar buttons (language/settings/gallery/
help/pin/min/max/close), the dock collapse toggle, and per-entry delete (x)
buttons were QSS border-radius rects — aliased corners on every hover. Route
them all through HoverPillButton, which paints the pill as an antialiased
rounded rect; QSS keeps them transparent and only sets glyph colour. Adds an
"active" pill state (property active == true) for the settings/pin toggles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tus pill)

The edit-toggle/copy buttons, the output container frame and the generation
status capsule set widget-level QSS border-radius (which both aliases and
overrides the app-level 9-patch rules). Route the buttons through
HoverPillButton, self-paint the frame (QSS keeps an identically-sized
transparent box so the layout doesn't move), and add PillLabel for the
capsule. Scrollbar handles drop from 4px to the app-wide rad_xs=2 radius.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, dock preview

Remaining always-visible QSS-rounded surfaces in the workbench:

- Prompt entry frames self-paint fill + hover border + the expanded-header
  tint (top-only rounded path); example entry frames self-paint fill + hover
  border. QSS keeps identically-sized transparent 1px-border boxes.
- Titlebar OC chips and the splitter swap pill route through HoverPillButton.
- The dock drop-preview band becomes a RoundedPanel (new shared class).
- Drop the dead #SwapButton rule (nothing sets that objectName).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shared floating popups still drew their rounded chrome via QSS: the panel
container, close/OK/Cancel buttons, the changelog popup and the list/spinbox
inside popup panels. Migrate them:

- _create_popup's inner panel and the changelog surface become RoundedPanel;
  the changelog body QTextEdit gets the 9-patch AA surface.
- PopupClose/PopupBtn route through HoverPillButton; QSS keeps glyph colour
  only (padding compensates the dropped 1px border).
- #PopupPanel QListWidget/::item/QSpinBox re-skinned via control_surfaces_qss
  9-patch overrides (items keep hover/selected states).
- The settings export/import QDialog drops its aliased QSS radius — it is a
  system-chromed window (macOS rounds it); its three group cards become
  RoundedPanels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…adata/timeline)

- Prompt-preview and shortcuts popup surfaces become RoundedPanel; the
  shortcuts tutorial button becomes a HoverPillButton pill.
- Prompt-preview message boxes and history-sidebar text blocks re-skin via
  the 9-patch AA surface (padding compensates border-width==slice; the
  history rule is scoped to QTextEdit so scrollbars don't inherit it).
- Metadata-viewer thumbnail frame self-paints its rounded border under the
  pixmap (border-image would be covered by the full-size pixmap).
- Timeline history cards become a self-painted _TimelineCard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e manager chrome

- Library artist/OC banner headers/bodies self-paint their asymmetric
  corners (open-edged paths so expanded header + body join seamlessly);
  ref-image thumbnails self-paint under the pixmap; the Artist/OC segmented
  pair and add-entry buttons become HoverPillButtons with an active state.
- OC bubble: circular avatar and stepper containers self-paint; outfit
  pills and action buttons become HoverPillButtons (active outfit keeps its
  accent state via the property).
- Tag-completer category dot becomes a self-painted AA circle.
- Image manager: the titlebar band merges into the self-painted window
  surface (top-only rounded band, continuous border); detail-panel action
  buttons become HoverPillButtons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The upstream CI (ci.yml, runs on every PR targeting master) gates on
ruff check — these seven leftovers from swapping QSS controls to the
self-painted classes were failing it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The window shell corners were the last QSS-rounded containers: the shell
fill+border (#WindowSurface), the title bar's top corners and the content
area's bottom corners — visible as aliased window corners on the frameless
Windows/Linux path. WindowSurface, a new _TitleBarSurface and
_ContentHostSurface now paint them antialiased (rounded_rect_path gains
bottom_only). On the macOS expanded-client-area path the native mask rounds
the window, so WindowSurface.set_plain(True) replaces the old QSS override
with a square fill. The title bar's 1px bottom hairline stays QSS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The interrogator panel read as square: its footer/action buttons and LLM
thumbnails used border-radius 0, the mode switch and inputs 2px. Bring it
into the subtle-round scale with antialiased surfaces:

- _local_button_style and the LLM text buttons (default/primary/danger/
  secondary) render via the AA 9-patch at rad_sm; padding compensates so
  button heights are unchanged. Compact/icon/toggle buttons stay in the
  small-control rad_xs tier (slice would eat their content box).
- The mode switch self-paints its frame and checked/hover segment fills
  (a 9-patch border-width would inflate the fixed 24px height).
- Inputs/combos/text edits move to the rad_sm 9-patch surface.
- LLM thumbnails self-paint rad_sm frames (selected keeps the 2px ring);
  the dashed add pad and the restart button join the same tier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finish the sweep on the rarely-shown chrome: updater dialogs (changelog
box, outline + accent action buttons), crash-report QMessageBox buttons,
the destroy-template editor (add/del/save/cancel buttons, list with
selected-item surface, name/text inputs), the onboarding tour panel and
its Next button, and the hint bubble surface. Buttons ride HoverPillButton
or the AA 9-patch with padding compensation; panels become RoundedPanels.
Progress bars and 2px-radius chips stay QSS (no visible aliasing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first pass skinned interrogator buttons with the 9-patch: outline-style
fills and stretched corners that read rounder than the QPainter-drawn pills
used everywhere else. Route every interrogator button (local setup/footer,
LLM toolbar/presets/footer) through HoverPillButton instead — the same
class as the workbench copy/edit buttons, so the shape matches exactly:
solid bg_surface fill + hairline border, accent for primaries, danger ring
while running. HoverPillButton now recognises checkable buttons' checked
state as 'active' (API toggle). The mode-switch segment fill drops to
rad_xs+1 — the near-capsule block read too round against the app's rounded
squares. The 9-patch stays only on the input family.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With the 9-patch border-image on combos, macOS' overlay popup geometry
(menu-style, content-width, aligned over the control) miscalculates its
height and the list opens cut off — and the QSS combobox-popup hint is not
honoured on the real interaction path. Give every QComboBox a Fusion BASE
style via an application-level Polish hook: its popup is the standard
below-the-control drop-down, sized correctly; the app stylesheet still
wraps the widget style, so the themed rendering is unchanged. Covers
combos created later (prompt entries, dialogs) too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant