feat(*): implement v0.1.0, add docs

This commit is contained in:
h
2026-01-19 01:25:44 +01:00
commit 506a975052
34 changed files with 3285 additions and 0 deletions

26
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: "Publish"
on:
push:
tags:
- v*
jobs:
run:
runs-on: ubuntu-latest
environment:
name: pypi
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install Python 3.13
run: uv python install 3.13
- name: Build
run: uv build
- name: Publish
run: uv publish

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
# Development
.idea
.vscode
# Docs
docs/_build/
docs/_static/
docs/_templates/

17
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,17 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.8
hooks:
- id: ruff-check
types_or: [ python, pyi ]
args: [ --fix ]
- id: ruff-format
types_or: [ python, pyi ]
- repo: local
hooks:
- id: ty
name: ty check
entry: uvx ty check
language: python
types_or: [ python, pyi ]

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

20
LICENSE.md Normal file
View File

@@ -0,0 +1,20 @@
Copyright (c) 2026 h
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

84
README.md Normal file
View File

@@ -0,0 +1,84 @@
# pyqt-liquidglass
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md)
macOS Liquid Glass effects for PySide6 and PyQt6.
![Screenshot](assets/screenshot.png)
## Overview
pyqt-liquidglass provides a Python API to apply Apple's native glass visual effects to Qt windows and widgets. On macOS 26+, it uses `NSGlassEffectView` for Liquid Glass. On older versions, it falls back to `NSVisualEffectView`.
## Features
- Apply glass effects to entire windows or specific widgets
- Configure corner radius, padding, and materials
- Reposition, hide, or show window traffic lights
- Automatic Qt binding detection (PySide6, PyQt6)
- Safe no-ops on non-macOS platforms
## Installation
```bash
pip install pyqt-liquidglass
```
Or with uv:
```bash
uv add pyqt-liquidglass
```
## Quick Start
```python
from PySide6.QtWidgets import QApplication, QMainWindow
import pyqt_liquidglass as glass
app = QApplication([])
window = QMainWindow()
window.resize(800, 600)
# Prepare before showing
glass.prepare_window_for_glass(window)
window.show()
# Apply glass after showing
glass.apply_glass_to_window(window)
app.exec()
```
### Sidebar Pattern
```python
# Apply glass to a sidebar widget
glass.apply_glass_to_widget(sidebar, options=glass.GlassOptions.sidebar())
# Position traffic lights
glass.setup_traffic_lights_inset(window, x_offset=18, y_offset=12)
```
### Custom Options
```python
options = glass.GlassOptions(
corner_radius=16.0,
padding=(10, 10, 10, 10),
)
glass.apply_glass_to_window(window, options=options)
```
## Requirements
- Python 3.12+
- macOS
- PySide6 or PyQt6
Tested with PySide6. PyQt6 should work but is not explicitly tested.
## License
MIT

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

40
docs/api.rst Normal file
View File

@@ -0,0 +1,40 @@
API Reference
=============
This page documents the public API of pyqt-liquidglass.
pyqt_liquidglass
----------------
.. automodule:: pyqt_liquidglass
:members:
:undoc-members:
:show-inheritance:
:exclude-members: __all__
Platform Constants
------------------
.. py:data:: pyqt_liquidglass.IS_MACOS
:type: bool
:noindex:
``True`` if running on macOS, ``False`` otherwise.
.. py:data:: pyqt_liquidglass.MACOS_VERSION
:type: tuple[int, int, int] | None
:noindex:
macOS version as a tuple (major, minor, patch), or ``None`` on other platforms.
.. py:data:: pyqt_liquidglass.HAS_GLASS_EFFECT
:type: bool
:noindex:
``True`` if ``NSGlassEffectView`` is available (macOS 26+).
.. py:data:: pyqt_liquidglass.HAS_VISUAL_EFFECT
:type: bool
:noindex:
``True`` if ``NSVisualEffectView`` is available.

55
docs/conf.py Normal file
View File

@@ -0,0 +1,55 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
import tomllib
sys.path.insert(0, os.path.abspath("."))
sys.path.insert(0, os.path.abspath("../."))
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "pyqt-liquidglass"
copyright = "2025, h"
author = "h"
release = tomllib.load(open("../pyproject.toml", "rb"))["project"]["version"]
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinx_autodoc_typehints",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
autodoc_default_options = {
"members": True,
"member-order": "bysource",
"special-members": "__init__",
"undoc-members": True,
"exclude-members": "__weakref__",
"inherited-members": False,
}
autodoc_typehints = "description"
typehints_use_signature = True
typehints_use_signature_return = True
autodoc_member_order = "bysource"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "furo"
html_static_path = ["_static"]
html_title = "pyqt-liquidglass"
html_theme_options = {"sidebar_hide_name": False, "navigation_with_keys": True}

200
docs/core_concepts.rst Normal file
View File

@@ -0,0 +1,200 @@
Core Concepts
=============
This page explains how pyqt-liquidglass works under the hood.
How Glass Effects Work
----------------------
Native Views
~~~~~~~~~~~~
macOS provides two classes for blur/glass effects:
- **NSGlassEffectView**: New in macOS 26 (Tahoe), provides the Liquid Glass effect
- **NSVisualEffectView**: Available since macOS 10.10, provides vibrancy effects
pyqt-liquidglass automatically uses ``NSGlassEffectView`` when available and falls back to ``NSVisualEffectView`` on older systems.
View Hierarchy
~~~~~~~~~~~~~~
Qt widgets map to native ``NSView`` objects. When you apply a glass effect, the library:
1. Gets the native ``NSView`` for your Qt widget
2. Creates a glass effect view (``NSGlassEffectView`` or ``NSVisualEffectView``)
3. Inserts it behind your widget's view in the z-order
4. Configures autoresizing so it tracks widget size changes
The glass view renders the blur effect, and your Qt content draws on top.
Window vs Widget Glass
----------------------
Window Glass
~~~~~~~~~~~~
``apply_glass_to_window()`` fills the entire window content area with glass:
.. code-block:: python
glass.apply_glass_to_window(window)
This is ideal for:
- Full-window blur backgrounds
- Floating panels
- Modal dialogs
The function configures the window for transparent titlebar and full-size content view automatically.
Widget Glass
~~~~~~~~~~~~
``apply_glass_to_widget()`` applies glass to a specific widget region:
.. code-block:: python
glass.apply_glass_to_widget(sidebar, options=glass.GlassOptions.sidebar())
This is ideal for:
- Sidebars (like macOS System Settings)
- Navigation panels
- Toolbars
The glass view tracks the widget's position and size, updating automatically on resize or move.
GlassOptions Configuration
--------------------------
The ``GlassOptions`` dataclass controls glass effect appearance:
.. code-block:: python
from pyqt_liquidglass import GlassOptions
options = GlassOptions(
corner_radius=16.0, # Rounded corners (NSGlassEffectView only)
material=GlassMaterial.SIDEBAR, # Material type
blending_mode=BlendingMode.BEHIND_WINDOW, # Blending mode
padding=(10, 10, 10, 10), # Left, top, right, bottom
)
Attributes
~~~~~~~~~~
**corner_radius** (float)
Corner radius in points. Only applies to ``NSGlassEffectView`` on macOS 26+. Default: 0.0
**material** (GlassMaterial)
The visual effect material. Only applies to ``NSVisualEffectView`` fallback. Options include:
- ``TITLEBAR``, ``SIDEBAR``, ``MENU``, ``POPOVER``
- ``SHEET``, ``WINDOW_BACKGROUND``, ``HUD``, ``TOOLTIP``
- ``CONTENT_BACKGROUND``, ``UNDER_WINDOW_BACKGROUND``
**blending_mode** (BlendingMode)
How the effect blends with content:
- ``BEHIND_WINDOW``: Blurs content behind the window
- ``WITHIN_WINDOW``: Blurs content within the window
**padding** (tuple)
Inset from widget edges in points: (left, top, right, bottom). Default: (0, 0, 0, 0)
Preset Methods
~~~~~~~~~~~~~~
For common use cases:
.. code-block:: python
# Full window glass (no corner radius, no padding)
options = GlassOptions.window()
# Sidebar glass (10pt radius, 9pt padding)
options = GlassOptions.sidebar()
# Custom sidebar
options = GlassOptions.sidebar(corner_radius=16.0, padding=12.0)
Coordinate Systems
------------------
Qt and Cocoa use different coordinate systems:
- **Qt**: Origin at top-left, Y increases downward
- **Cocoa**: Origin at bottom-left, Y increases upward
pyqt-liquidglass handles this conversion internally. When specifying ``y_offset`` for traffic lights, positive values move the buttons down from center.
Traffic Lights
--------------
The traffic lights are the close, minimize, and zoom buttons in the window titlebar.
Positioning
~~~~~~~~~~~
``setup_traffic_lights_inset()`` repositions the buttons using ``NSLayoutConstraint``:
.. code-block:: python
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=12)
- **x_offset**: Distance from the left edge in points
- **y_offset**: Vertical offset from center (positive = down)
This method survives window resizes because it uses Auto Layout constraints rather than absolute positioning.
Visibility
~~~~~~~~~~
Hide the buttons while keeping window functionality:
.. code-block:: python
glass.hide_traffic_lights(window)
glass.show_traffic_lights(window)
The window remains closable, minimizable, and zoomable via keyboard shortcuts and menu commands.
Platform Detection
------------------
The library provides constants for platform detection:
.. code-block:: python
from pyqt_liquidglass import (
IS_MACOS, # True if running on macOS
MACOS_VERSION, # Tuple like (15, 1, 0) or None
HAS_GLASS_EFFECT, # True if NSGlassEffectView is available
HAS_VISUAL_EFFECT, # True if NSVisualEffectView is available
)
All glass functions are safe to call on non-macOS platforms. They return ``None`` or ``False`` without side effects.
Effect Lifecycle
----------------
Effect IDs
~~~~~~~~~~
``apply_glass_to_window()`` and ``apply_glass_to_widget()`` return an effect ID:
.. code-block:: python
effect_id = glass.apply_glass_to_window(window)
Use this ID to remove the effect later:
.. code-block:: python
glass.remove_glass_effect(effect_id)
Cleanup
~~~~~~~
When a window is closed, Qt destroys the widget hierarchy. The glass effect views are removed automatically as part of the native view cleanup. You don't need to manually remove effects before closing windows.

544
docs/examples.rst Normal file
View File

@@ -0,0 +1,544 @@
Examples
========
This page contains complete, runnable examples demonstrating various use cases.
Full Window Glass
-----------------
The simplest example: glass filling the entire window content area.
.. code-block:: python
"""Full window glass effect example."""
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
import pyqt_liquidglass as glass
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Window Glass")
self.resize(600, 400)
central = QWidget()
central.setStyleSheet("background: transparent;")
layout = QVBoxLayout(central)
layout.setContentsMargins(40, 60, 40, 40)
label = QLabel("Hello, Liquid Glass!")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet("""
font-size: 28px;
font-weight: 600;
color: white;
background: transparent;
""")
layout.addWidget(label)
self.setCentralWidget(central)
def main() -> int:
app = QApplication(sys.argv)
window = MainWindow()
# Prepare window BEFORE showing
glass.prepare_window_for_glass(window)
window.show()
# Apply glass AFTER showing
glass.apply_glass_to_window(window)
return app.exec()
if __name__ == "__main__":
sys.exit(main())
Sidebar Pattern
---------------
Settings-style window with a glass sidebar and opaque content area. This is the most common pattern for macOS applications.
.. code-block:: python
"""Sidebar with glass effect example."""
import sys
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QListWidget,
QListWidgetItem,
QMainWindow,
QVBoxLayout,
QWidget,
)
import pyqt_liquidglass as glass
class Sidebar(QWidget):
"""Transparent sidebar widget for glass effect."""
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setFixedWidth(250)
self.setStyleSheet("background: transparent;")
layout = QVBoxLayout(self)
layout.setContentsMargins(9, 18, 9, 9)
self.list_widget = QListWidget()
self.list_widget.setStyleSheet("""
QListWidget {
font-size: 13px;
background: transparent;
border: none;
outline: none;
}
QListWidget::item {
padding: 5px 2px;
margin: 0px 9px;
border-radius: 4px;
}
QListWidget::item:selected {
background: rgba(255, 255, 255, 0.1);
}
""")
self.list_widget.viewport().setStyleSheet("background: transparent;")
for item_text in ["General", "Appearance", "Sound", "Network", "Privacy"]:
self.list_widget.addItem(QListWidgetItem(item_text))
self.list_widget.setCurrentRow(0)
layout.addWidget(self.list_widget)
class Content(QWidget):
"""Opaque content area."""
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setStyleSheet("background-color: #1e1e1e;")
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
self.title = QLabel("General")
self.title.setStyleSheet("""
font-size: 18px;
font-weight: bold;
color: white;
background: transparent;
""")
self.description = QLabel("Configure general application settings.")
self.description.setStyleSheet("""
font-size: 14px;
color: #888888;
background: transparent;
""")
layout.addWidget(self.title)
layout.addSpacing(8)
layout.addWidget(self.description)
layout.addStretch()
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Settings")
self.resize(720, 600)
central = QWidget()
layout = QHBoxLayout(central)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.sidebar = Sidebar()
self.content = Content()
layout.addWidget(self.sidebar)
layout.addWidget(self.content, 1)
self.setCentralWidget(central)
def main() -> int:
app = QApplication(sys.argv)
window = MainWindow()
glass.prepare_window_for_glass(window)
window.show()
# Inset traffic lights to sit nicely on sidebar
glass.setup_traffic_lights_inset(window, x_offset=18, y_offset=12)
# Apply glass to sidebar with rounded corners
glass.apply_glass_to_widget(window.sidebar, options=glass.GlassOptions.sidebar())
return app.exec()
if __name__ == "__main__":
sys.exit(main())
Frameless Floating Panel
------------------------
A frameless, draggable panel useful for HUDs, tool palettes, or popovers.
.. code-block:: python
"""Frameless window with glass effect example."""
import sys
from PySide6.QtCore import QPoint, Qt
from PySide6.QtGui import QMouseEvent
from PySide6.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget
import pyqt_liquidglass as glass
class FloatingPanel(QWidget):
"""A frameless, draggable floating panel with glass effect."""
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Floating Panel")
self.resize(300, 200)
self._drag_position: QPoint | None = None
self.setStyleSheet("background: transparent;")
layout = QVBoxLayout(self)
layout.setContentsMargins(24, 24, 24, 24)
layout.setSpacing(16)
title = QLabel("Floating Panel")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("""
font-size: 18px;
font-weight: 600;
color: white;
background: transparent;
""")
subtitle = QLabel("Drag anywhere to move")
subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter)
subtitle.setStyleSheet("""
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
background: transparent;
""")
close_button = QPushButton("Close")
close_button.setStyleSheet("""
QPushButton {
background: rgba(255, 255, 255, 0.15);
border: none;
border-radius: 8px;
padding: 10px 24px;
color: white;
font-size: 13px;
}
QPushButton:hover {
background: rgba(255, 255, 255, 0.25);
}
QPushButton:pressed {
background: rgba(255, 255, 255, 0.1);
}
""")
close_button.clicked.connect(self.close)
layout.addWidget(title)
layout.addWidget(subtitle)
layout.addStretch()
layout.addWidget(close_button)
def mousePressEvent(self, event: QMouseEvent) -> None:
if event.button() == Qt.MouseButton.LeftButton:
self._drag_position = (
event.globalPosition().toPoint() - self.frameGeometry().topLeft()
)
event.accept()
def mouseMoveEvent(self, event: QMouseEvent) -> None:
if (
event.buttons() == Qt.MouseButton.LeftButton
and self._drag_position is not None
):
self.move(event.globalPosition().toPoint() - self._drag_position)
event.accept()
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
self._drag_position = None
def main() -> int:
app = QApplication(sys.argv)
panel = FloatingPanel()
# Prepare with frameless=True to remove window decorations
glass.prepare_window_for_glass(panel, frameless=True)
panel.show()
# Apply glass with rounded corners for the floating look
glass.apply_glass_to_window(panel, options=glass.GlassOptions(corner_radius=16.0))
return app.exec()
if __name__ == "__main__":
sys.exit(main())
Custom GlassOptions
-------------------
Demonstrates different glass configurations side by side.
.. code-block:: python
"""Custom GlassOptions example."""
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
import pyqt_liquidglass as glass
class GlassPanel(QWidget):
"""A panel that will have glass applied to it."""
def __init__(self, title: str, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setStyleSheet("background: transparent;")
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
label = QLabel(title)
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet("""
font-size: 16px;
font-weight: 600;
color: white;
background: transparent;
""")
layout.addWidget(label)
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Custom Glass Options")
self.resize(800, 400)
central = QWidget()
central.setStyleSheet("background: transparent;")
layout = QHBoxLayout(central)
layout.setContentsMargins(20, 60, 20, 20)
layout.setSpacing(20)
self.panel_default = GlassPanel("Default\n(no radius)")
self.panel_default.setFixedWidth(200)
self.panel_rounded = GlassPanel("Rounded\n(radius: 16)")
self.panel_rounded.setFixedWidth(200)
self.panel_padded = GlassPanel("Padded\n(padding: 20)")
self.panel_padded.setFixedWidth(200)
layout.addWidget(self.panel_default)
layout.addWidget(self.panel_rounded)
layout.addWidget(self.panel_padded)
self.setCentralWidget(central)
def main() -> int:
app = QApplication(sys.argv)
window = MainWindow()
glass.prepare_window_for_glass(window)
window.show()
# Apply window glass as background
glass.apply_glass_to_window(window)
# Apply different glass options to each panel
glass.apply_glass_to_widget(
window.panel_default,
options=glass.GlassOptions(corner_radius=0.0, padding=(8, 8, 8, 8)),
)
glass.apply_glass_to_widget(
window.panel_rounded,
options=glass.GlassOptions(corner_radius=16.0, padding=(8, 8, 8, 8)),
)
glass.apply_glass_to_widget(
window.panel_padded,
options=glass.GlassOptions(corner_radius=12.0, padding=(20, 20, 20, 20)),
)
return app.exec()
if __name__ == "__main__":
sys.exit(main())
Traffic Lights Control
----------------------
Demonstrates hiding, showing, and repositioning the macOS window buttons.
.. code-block:: python
"""Traffic lights control example."""
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
import pyqt_liquidglass as glass
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Traffic Lights Demo")
self.resize(500, 300)
self._lights_visible = True
central = QWidget()
central.setStyleSheet("background: transparent;")
layout = QVBoxLayout(central)
layout.setContentsMargins(40, 80, 40, 40)
layout.setSpacing(20)
title = QLabel("Traffic Lights Control")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet("""
font-size: 22px;
font-weight: 600;
color: white;
background: transparent;
""")
description = QLabel(
"The traffic lights have been repositioned.\n"
"Use the button below to hide or show them."
)
description.setAlignment(Qt.AlignmentFlag.AlignCenter)
description.setStyleSheet("""
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
background: transparent;
""")
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(12)
self.toggle_button = QPushButton("Hide Traffic Lights")
self.toggle_button.setStyleSheet("""
QPushButton {
background: rgba(255, 255, 255, 0.15);
border: none;
border-radius: 8px;
padding: 12px 24px;
color: white;
font-size: 13px;
min-width: 160px;
}
QPushButton:hover {
background: rgba(255, 255, 255, 0.25);
}
QPushButton:pressed {
background: rgba(255, 255, 255, 0.1);
}
""")
self.toggle_button.clicked.connect(self._toggle_traffic_lights)
buttons_layout.addStretch()
buttons_layout.addWidget(self.toggle_button)
buttons_layout.addStretch()
layout.addWidget(title)
layout.addWidget(description)
layout.addStretch()
layout.addLayout(buttons_layout)
self.setCentralWidget(central)
def _toggle_traffic_lights(self) -> None:
if self._lights_visible:
glass.hide_traffic_lights(self)
self.toggle_button.setText("Show Traffic Lights")
else:
glass.show_traffic_lights(self)
self.toggle_button.setText("Hide Traffic Lights")
self._lights_visible = not self._lights_visible
def main() -> int:
app = QApplication(sys.argv)
window = MainWindow()
glass.prepare_window_for_glass(window)
window.show()
# Apply glass to window
glass.apply_glass_to_window(window)
# Reposition traffic lights with custom offset
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=16)
return app.exec()
if __name__ == "__main__":
sys.exit(main())

151
docs/getting_started.rst Normal file
View File

@@ -0,0 +1,151 @@
Getting Started
===============
This guide covers installation and basic usage of pyqt-liquidglass.
Installation
------------
Using pip
~~~~~~~~~
.. code-block:: bash
pip install pyqt-liquidglass
Using uv
~~~~~~~~
.. code-block:: bash
uv add pyqt-liquidglass
Requirements
------------
- **Python**: 3.12 or higher
- **Operating System**: macOS (functions are safe no-ops on other platforms)
- **Qt Binding**: PySide6 or PyQt6
The library automatically detects your Qt binding.
Quick Start
-----------
The Three-Step Pattern
~~~~~~~~~~~~~~~~~~~~~~
Every glass effect follows the same pattern:
1. **Prepare** the window before showing
2. **Show** the window
3. **Apply** the glass effect
.. code-block:: python
import pyqt_liquidglass as glass
# 1. Prepare before showing
glass.prepare_window_for_glass(window)
# 2. Show the window
window.show()
# 3. Apply glass after showing
glass.apply_glass_to_window(window)
Full Window Example
~~~~~~~~~~~~~~~~~~~
.. code-block:: python
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
import pyqt_liquidglass as glass
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Glass Demo")
self.resize(600, 400)
central = QWidget()
central.setStyleSheet("background: transparent;")
layout = QVBoxLayout(central)
layout.setContentsMargins(40, 60, 40, 40)
label = QLabel("Hello, Liquid Glass!")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet("""
font-size: 28px;
font-weight: 600;
color: white;
background: transparent;
""")
layout.addWidget(label)
self.setCentralWidget(central)
def main() -> int:
app = QApplication(sys.argv)
window = MainWindow()
glass.prepare_window_for_glass(window)
window.show()
glass.apply_glass_to_window(window)
return app.exec()
if __name__ == "__main__":
sys.exit(main())
Key Points
~~~~~~~~~~
- Set ``background: transparent`` on widgets that should show the glass through
- Call ``prepare_window_for_glass()`` before ``show()``
- Call ``apply_glass_to_window()`` after ``show()``
- The window needs to be visible for the native view hierarchy to exist
Widget Glass
~~~~~~~~~~~~
For applying glass to specific widgets (like sidebars):
.. code-block:: python
sidebar = QWidget()
sidebar.setFixedWidth(250)
sidebar.setStyleSheet("background: transparent;")
# After window.show():
glass.apply_glass_to_widget(sidebar, options=glass.GlassOptions.sidebar())
Traffic Lights
~~~~~~~~~~~~~~
Reposition the macOS window buttons:
.. code-block:: python
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=12)
Hide or show them:
.. code-block:: python
glass.hide_traffic_lights(window)
glass.show_traffic_lights(window)
Next Steps
----------
- Learn about :doc:`core_concepts` to understand how glass effects work
- See :doc:`examples` for complete working examples
- Check the :doc:`api` for detailed function signatures

89
docs/index.rst Normal file
View File

@@ -0,0 +1,89 @@
pyqt-liquidglass
================
**macOS Liquid Glass effects for PySide6 and PyQt6**
pyqt-liquidglass brings Apple's Liquid Glass visual effects to your Qt applications on macOS. It provides a clean Python API to apply the native ``NSGlassEffectView`` (macOS 26+) or ``NSVisualEffectView`` (fallback) to windows and widgets.
----
Features
--------
- **Window Glass**: Apply glass effects to entire windows
- **Widget Glass**: Target specific widgets like sidebars or panels
- **Traffic Lights Control**: Reposition, hide, or show window buttons
- **GlassOptions**: Configure corner radius, padding, materials, and blending
- **Cross-Version**: Uses ``NSGlassEffectView`` on macOS 26+, falls back to ``NSVisualEffectView``
- **Safe No-ops**: All functions work on non-macOS platforms (return ``None`` or ``False``)
Quick Example
-------------
.. code-block:: python
from PySide6.QtWidgets import QApplication, QMainWindow
import pyqt_liquidglass as glass
app = QApplication([])
window = QMainWindow()
window.resize(800, 600)
# Prepare before showing
glass.prepare_window_for_glass(window)
window.show()
# Apply glass after showing
glass.apply_glass_to_window(window)
app.exec()
Installation
------------
.. code-block:: bash
pip install pyqt-liquidglass
Or with uv:
.. code-block:: bash
uv add pyqt-liquidglass
**Requirements**: Python 3.12+, macOS, PySide6 or PyQt6
----
Documentation
=============
.. toctree::
:maxdepth: 2
:caption: User Guide
getting_started
core_concepts
examples
.. toctree::
:maxdepth: 2
:caption: API Reference
api
----
Indices
=======
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
----
License
=======
MIT License. See LICENSE.md for details.

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

3
docs/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
sphinx
furo
sphinx-autodoc-typehints

1
examples/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Example scripts for pyqt-liquidglass."""

111
examples/custom_options.py Normal file
View File

@@ -0,0 +1,111 @@
"""Custom GlassOptions example.
Demonstrates how to configure glass effects with custom parameters
including corner radius, padding, and materials.
"""
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
import pyqt_liquidglass as glass
class GlassPanel(QWidget):
"""A panel that will have glass applied to it."""
def __init__(self, title: str, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setStyleSheet("background: transparent;")
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
label = QLabel(title)
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet(
"""
font-size: 16px;
font-weight: 600;
color: white;
background: transparent;
"""
)
layout.addWidget(label)
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Custom Glass Options")
self.resize(800, 400)
central = QWidget()
central.setStyleSheet("background: transparent;")
layout = QHBoxLayout(central)
layout.setContentsMargins(20, 60, 20, 20)
layout.setSpacing(20)
# Three panels with different glass options
self.panel_default = GlassPanel("Default\n(no radius)")
self.panel_default.setFixedWidth(200)
self.panel_rounded = GlassPanel("Rounded\n(radius: 16)")
self.panel_rounded.setFixedWidth(200)
self.panel_padded = GlassPanel("Padded\n(padding: 20)")
self.panel_padded.setFixedWidth(200)
layout.addWidget(self.panel_default)
layout.addWidget(self.panel_rounded)
layout.addWidget(self.panel_padded)
self.setCentralWidget(central)
def main() -> int:
app = QApplication(sys.argv)
window = MainWindow()
glass.prepare_window_for_glass(window)
window.show()
# Apply window glass as background
glass.apply_glass_to_window(window)
# Apply different glass options to each panel
# Panel 1: Default options (no corner radius)
glass.apply_glass_to_widget(
window.panel_default,
options=glass.GlassOptions(corner_radius=0.0, padding=(8, 8, 8, 8)),
)
# Panel 2: Rounded corners
glass.apply_glass_to_widget(
window.panel_rounded,
options=glass.GlassOptions(corner_radius=16.0, padding=(8, 8, 8, 8)),
)
# Panel 3: Large padding
glass.apply_glass_to_widget(
window.panel_padded,
options=glass.GlassOptions(corner_radius=12.0, padding=(20, 20, 20, 20)),
)
return app.exec()
if __name__ == "__main__":
sys.exit(main())

115
examples/frameless.py Normal file
View File

@@ -0,0 +1,115 @@
"""Frameless window with glass effect example.
A floating panel without standard window decorations, useful for
custom UI elements like popovers, HUDs, or tool palettes.
"""
import sys
from PySide6.QtCore import QPoint, Qt
from PySide6.QtGui import QMouseEvent
from PySide6.QtWidgets import QApplication, QLabel, QPushButton, QVBoxLayout, QWidget
import pyqt_liquidglass as glass
class FloatingPanel(QWidget):
"""A frameless, draggable floating panel with glass effect."""
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Floating Panel")
self.resize(300, 200)
self._drag_position: QPoint | None = None
self.setStyleSheet("background: transparent;")
layout = QVBoxLayout(self)
layout.setContentsMargins(24, 24, 24, 24)
layout.setSpacing(16)
title = QLabel("Floating Panel")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet(
"""
font-size: 18px;
font-weight: 600;
color: white;
background: transparent;
"""
)
subtitle = QLabel("Drag anywhere to move")
subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter)
subtitle.setStyleSheet(
"""
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
background: transparent;
"""
)
close_button = QPushButton("Close")
close_button.setStyleSheet(
"""
QPushButton {
background: rgba(255, 255, 255, 0.15);
border: none;
border-radius: 8px;
padding: 10px 24px;
color: white;
font-size: 13px;
}
QPushButton:hover {
background: rgba(255, 255, 255, 0.25);
}
QPushButton:pressed {
background: rgba(255, 255, 255, 0.1);
}
"""
)
close_button.clicked.connect(self.close)
layout.addWidget(title)
layout.addWidget(subtitle)
layout.addStretch()
layout.addWidget(close_button)
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
if event.button() == Qt.MouseButton.LeftButton:
self._drag_position = (
event.globalPosition().toPoint() - self.frameGeometry().topLeft()
)
event.accept()
def mouseMoveEvent(self, event: QMouseEvent) -> None: # noqa: N802
if (
event.buttons() == Qt.MouseButton.LeftButton
and self._drag_position is not None
):
self.move(event.globalPosition().toPoint() - self._drag_position)
event.accept()
def mouseReleaseEvent(self, event: QMouseEvent) -> None: # noqa: N802, ARG002
self._drag_position = None
def main() -> int:
app = QApplication(sys.argv)
panel = FloatingPanel()
# Prepare with frameless=True to remove window decorations
glass.prepare_window_for_glass(panel, frameless=True)
panel.show()
# Apply glass with rounded corners for the floating look
glass.apply_glass_to_window(panel, options=glass.GlassOptions(corner_radius=16.0))
return app.exec()
if __name__ == "__main__":
sys.exit(main())

151
examples/sidebar.py Normal file
View File

@@ -0,0 +1,151 @@
"""Sidebar with glass effect example.
Settings-style window with a glass sidebar and opaque content area.
This is the most common pattern for using Liquid Glass.
"""
import sys
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QListWidget,
QListWidgetItem,
QMainWindow,
QVBoxLayout,
QWidget,
)
import pyqt_liquidglass as glass
class Sidebar(QWidget):
"""Transparent sidebar widget for glass effect."""
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setFixedWidth(250)
self.setStyleSheet("background: transparent;")
layout = QVBoxLayout(self)
# Top padding for traffic lights area
layout.setContentsMargins(9, 18, 9, 9)
self.list_widget = QListWidget()
self.list_widget.setStyleSheet("""
QListWidget {
font-size: 13px;
background: transparent;
border: none;
outline: none;
}
QListWidget::item {
padding: 5px 2px;
margin: 0px 9px;
border-radius: 4px;
}
QListWidget::item:selected {
background: rgba(255, 255, 255, 0.1);
}
""")
self.list_widget.viewport().setStyleSheet("background: transparent;")
for item_text in ["General", "Appearance", "Sound", "Network", "Privacy"]:
self.list_widget.addItem(QListWidgetItem(item_text))
self.list_widget.setCurrentRow(0)
layout.addWidget(self.list_widget)
class Content(QWidget):
"""Opaque content area."""
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setStyleSheet("background-color: #1e1e1e;")
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 0, 20, 20)
self.title = QLabel("General")
self.title.setStyleSheet(
"""
font-size: 18px;
font-weight: bold;
color: white;
background: transparent;
"""
)
self.description = QLabel("Configure general application settings.")
self.description.setStyleSheet(
"""
font-size: 14px;
color: #888888;
background: transparent;
"""
)
layout.addWidget(self.title)
layout.addSpacing(8)
layout.addWidget(self.description)
layout.addStretch()
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Settings")
self.resize(720, 600)
central = QWidget()
layout = QHBoxLayout(central)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.sidebar = Sidebar()
self.content = Content()
layout.addWidget(self.sidebar)
layout.addWidget(self.content, 1)
self.setCentralWidget(central)
self.sidebar.list_widget.currentRowChanged.connect(self._on_selection_changed)
def _on_selection_changed(self, row: int) -> None:
items = ["General", "Appearance", "Sound", "Network", "Privacy"]
descriptions = [
"Configure general application settings.",
"Customize colors, fonts, and themes.",
"Adjust volume and audio devices.",
"Manage network connections and proxy.",
"Control permissions and data sharing.",
]
if 0 <= row < len(items):
self.content.title.setText(items[row])
self.content.description.setText(descriptions[row])
def main() -> int:
app = QApplication(sys.argv)
window = MainWindow()
# Prepare window before showing
glass.prepare_window_for_glass(window)
window.show()
# Inset traffic lights to sit nicely on sidebar
glass.setup_traffic_lights_inset(window, x_offset=18, y_offset=12)
# Apply glass to sidebar with rounded corners
glass.apply_glass_to_widget(window.sidebar, options=glass.GlassOptions.sidebar())
return app.exec()
if __name__ == "__main__":
sys.exit(main())

128
examples/traffic_lights.py Normal file
View File

@@ -0,0 +1,128 @@
"""Traffic lights control example.
Demonstrates hiding, showing, and repositioning the macOS window
traffic lights (close, minimize, zoom buttons).
"""
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
import pyqt_liquidglass as glass
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Traffic Lights Demo")
self.resize(500, 300)
self._lights_visible = True
central = QWidget()
central.setStyleSheet("background: transparent;")
layout = QVBoxLayout(central)
layout.setContentsMargins(40, 80, 40, 40)
layout.setSpacing(20)
title = QLabel("Traffic Lights Control")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
title.setStyleSheet(
"""
font-size: 22px;
font-weight: 600;
color: white;
background: transparent;
"""
)
description = QLabel(
"The traffic lights have been repositioned.\n"
"Use the buttons below to hide or show them."
)
description.setAlignment(Qt.AlignmentFlag.AlignCenter)
description.setStyleSheet(
"""
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
background: transparent;
"""
)
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(12)
self.toggle_button = QPushButton("Hide Traffic Lights")
self.toggle_button.setStyleSheet(self._button_style())
self.toggle_button.clicked.connect(self._toggle_traffic_lights)
buttons_layout.addStretch()
buttons_layout.addWidget(self.toggle_button)
buttons_layout.addStretch()
layout.addWidget(title)
layout.addWidget(description)
layout.addStretch()
layout.addLayout(buttons_layout)
self.setCentralWidget(central)
def _button_style(self) -> str:
return """
QPushButton {
background: rgba(255, 255, 255, 0.15);
border: none;
border-radius: 8px;
padding: 12px 24px;
color: white;
font-size: 13px;
min-width: 160px;
}
QPushButton:hover {
background: rgba(255, 255, 255, 0.25);
}
QPushButton:pressed {
background: rgba(255, 255, 255, 0.1);
}
"""
def _toggle_traffic_lights(self) -> None:
if self._lights_visible:
glass.hide_traffic_lights(self)
self.toggle_button.setText("Show Traffic Lights")
else:
glass.show_traffic_lights(self)
self.toggle_button.setText("Hide Traffic Lights")
self._lights_visible = not self._lights_visible
def main() -> int:
app = QApplication(sys.argv)
window = MainWindow()
glass.prepare_window_for_glass(window)
window.show()
# Apply glass to window
glass.apply_glass_to_window(window)
# Reposition traffic lights with custom offset
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=16)
return app.exec()
if __name__ == "__main__":
sys.exit(main())

59
examples/window_glass.py Normal file
View File

@@ -0,0 +1,59 @@
"""Full window glass effect example.
The simplest possible example: a window with glass filling the entire
content area.
"""
import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
import pyqt_liquidglass as glass
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Window Glass")
self.resize(600, 400)
central = QWidget()
central.setStyleSheet("background: transparent;")
layout = QVBoxLayout(central)
layout.setContentsMargins(40, 60, 40, 40)
label = QLabel("Hello, Liquid Glass!")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet(
"""
font-size: 28px;
font-weight: 600;
color: white;
background: transparent;
"""
)
layout.addWidget(label)
self.setCentralWidget(central)
def main() -> int:
app = QApplication(sys.argv)
window = MainWindow()
# Prepare window BEFORE showing
glass.prepare_window_for_glass(window)
window.show()
# Apply glass AFTER showing
glass.apply_glass_to_window(window)
return app.exec()
if __name__ == "__main__":
sys.exit(main())

45
pyproject.toml Normal file
View File

@@ -0,0 +1,45 @@
[project]
name = "pyqt-liquidglass"
version = "0.1.0"
description = "Simple library for PyQt to add macOS liquid glass effect"
readme = "README.md"
authors = [
{ name = "h", email = "h@kotikot.com" }
]
requires-python = ">=3.13"
dependencies = [
"pyobjc-framework-cocoa>=12.1",
"pyobjc-framework-quartz>=12.1",
]
[build-system]
requires = ["uv_build>=0.9.13,<0.10.0"]
build-backend = "uv_build"
[dependency-groups]
dev = [
"pyside6-essentials>=6.10.1",
"pyside6-stubs>=6.7.3.0",
]
[tool.ruff]
target-version = "py313"
[tool.ruff.lint]
select = ["ALL"]
ignore = ["CPY", "D1", "D203", "D212", "COM812", "RUF001", "RUF002", "RUF003"]
unfixable = ["F401"]
[tool.ruff.lint.per-file-ignores]
"*.lock" = ["ALL"]
"docs/*" = ["ALL"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.format]
docstring-code-format = true
skip-magic-trailing-comma = true
[tool.ruff.lint.isort]
split-on-trailing-comma = false

View File

@@ -0,0 +1,75 @@
"""
pyqt-liquidglass: macOS Liquid Glass effects for PySide6/PyQt6.
This library provides a simple API to apply Apple's Liquid Glass visual
effects to Qt windows and widgets on macOS. On non-macOS platforms, all
functions are safe no-ops that return None or False.
Basic Usage::
from PySide6.QtWidgets import QApplication, QMainWindow
import pyqt_liquidglass as glass
app = QApplication([])
window = QMainWindow()
window.setWindowTitle("Glass Demo")
window.resize(800, 600)
glass.prepare_window_for_glass(window)
window.show()
glass.apply_glass_to_window(window)
glass.setup_traffic_lights_inset(window, x_offset=20, y_offset=15)
app.exec()
Widget-specific glass (e.g., sidebar)::
sidebar = QWidget()
sidebar.setFixedWidth(200)
glass.prepare_widget_for_glass(sidebar)
# After window.show():
glass.apply_glass_to_widget(
sidebar, options=glass.GlassOptions.sidebar(corner_radius=12)
)
"""
from __future__ import annotations
__version__ = "0.1.0"
from ._platform import HAS_GLASS_EFFECT, HAS_VISUAL_EFFECT, IS_MACOS, MACOS_VERSION
from ._types import BlendingMode, GlassMaterial, GlassOptions
from .glass import apply_glass_to_widget, apply_glass_to_window, remove_glass_effect
from .helpers import (
prepare_widget_for_glass,
prepare_window_for_glass,
set_window_background_transparent,
)
from .traffic_lights import (
hide_traffic_lights,
setup_traffic_lights_inset,
show_traffic_lights,
)
__all__ = [
"HAS_GLASS_EFFECT",
"HAS_VISUAL_EFFECT",
"IS_MACOS",
"MACOS_VERSION",
"BlendingMode",
"GlassMaterial",
"GlassOptions",
"__version__",
"apply_glass_to_widget",
"apply_glass_to_window",
"hide_traffic_lights",
"prepare_widget_for_glass",
"prepare_window_for_glass",
"remove_glass_effect",
"set_window_background_transparent",
"setup_traffic_lights_inset",
"show_traffic_lights",
]

View File

@@ -0,0 +1,97 @@
"""Bridge utilities for converting Qt objects to native macOS Cocoa objects."""
from __future__ import annotations
from ctypes import c_void_p
from typing import TYPE_CHECKING, Any
from ._compat import QtWidgets, get_window_id
from ._platform import IS_MACOS
if TYPE_CHECKING:
from ._compat import QtCore
__all__ = [
"convert_qt_rect_to_ns_frame",
"get_nsview_from_widget",
"get_nswindow_from_widget",
]
def get_nsview_from_widget(widget: QtWidgets.QWidget) -> Any | None: # noqa: ANN401
"""
Get the NSView backing a QWidget.
Args:
widget: A Qt widget that has been realized (shown on screen).
Returns:
The NSView object (as a PyObjC object), or None if unavailable
or not on macOS.
Note:
The widget must have a valid window ID, which typically requires
the widget to be shown before calling this function.
"""
if not IS_MACOS:
return None
try:
import objc # noqa: PLC0415 # ty: ignore
win_id = get_window_id(widget)
return objc.objc_object(c_void_p=c_void_p(win_id)) # ty: ignore
except Exception: # noqa: BLE001
return None
def get_nswindow_from_widget(widget: QtWidgets.QWidget) -> Any | None: # noqa: ANN401
"""
Get the NSWindow containing a QWidget.
Args:
widget: A Qt widget that has been realized (shown on screen).
Returns:
The NSWindow object (as a PyObjC object), or None if unavailable
or not on macOS.
"""
ns_view = get_nsview_from_widget(widget)
if ns_view is None:
return None
try:
return ns_view.window() # ty: ignore
except Exception: # noqa: BLE001
return None
def convert_qt_rect_to_ns_frame(
qt_rect: QtCore.QRect, container_height: float, *, is_flipped: bool = False
) -> tuple[float, float, float, float]:
"""
Convert a Qt rectangle to Cocoa NSRect coordinates.
Qt uses a top-left origin coordinate system where Y increases downward.
Cocoa (when not flipped) uses a bottom-left origin where Y increases
upward.
Args:
qt_rect: Rectangle in Qt coordinates.
container_height: Height of the container view for Y-axis conversion.
is_flipped: Whether the container view uses flipped coordinates
(top-left origin like Qt). Default is False (standard Cocoa).
Returns:
A tuple of (x, y, width, height) in Cocoa coordinates.
"""
x = float(qt_rect.x())
width = float(qt_rect.width())
height = float(qt_rect.height())
if is_flipped:
y = float(qt_rect.y())
else:
y = container_height - float(qt_rect.y()) - height
return (x, y, width, height)

View File

@@ -0,0 +1,58 @@
"""Qt binding compatibility layer for PySide6 and PyQt6."""
from __future__ import annotations
from typing import Literal
__all__ = [
"PYQT6",
"PYSIDE6",
"QT_BINDING",
"QtCore",
"QtGui",
"QtWidgets",
"get_window_id",
]
PYSIDE6: bool = False
PYQT6: bool = False
QT_BINDING: Literal["PySide6", "PyQt6", "none"] = "none"
try:
from PySide6 import QtCore, QtGui, QtWidgets
PYSIDE6 = True
QT_BINDING = "PySide6"
except ImportError:
try:
from PyQt6 import QtCore, QtGui, QtWidgets
PYQT6 = True
QT_BINDING = "PyQt6"
except ImportError:
msg = (
"No Qt binding found. Install either PySide6 or PyQt6:\n"
" pip install PySide6\n"
" pip install PyQt6"
)
raise ImportError(msg) from None
def get_window_id(widget: QtWidgets.QWidget) -> int:
"""
Get the native window ID from a Qt widget.
Args:
widget: A Qt widget that has been realized (shown).
Returns:
The native window ID as an integer.
Note:
PySide6 returns an int directly, while PyQt6 returns a sip.voidptr
that needs to be converted.
"""
win_id = widget.winId()
if PYQT6:
return int(win_id) # type: ignore[arg-type]
return win_id # type: ignore[return-value]

View File

@@ -0,0 +1,104 @@
"""Platform detection and guards for macOS-specific functionality."""
from __future__ import annotations
import platform
import sys
from functools import wraps
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
__all__ = [
"HAS_GLASS_EFFECT",
"HAS_VISUAL_EFFECT",
"IS_MACOS",
"MACOS_VERSION",
"platform_guard",
"require_macos",
]
IS_MACOS: bool = sys.platform == "darwin"
MACOS_VERSION: tuple[int, int, int] | None = None
if IS_MACOS:
try:
version_str = platform.mac_ver()[0]
parts = version_str.split(".")
MACOS_VERSION = (
int(parts[0]) if len(parts) > 0 else 0,
int(parts[1]) if len(parts) > 1 else 0,
int(parts[2]) if len(parts) > 2 else 0, # noqa: PLR2004
)
except (ValueError, IndexError):
MACOS_VERSION = (0, 0, 0)
_MIN_VISUAL_EFFECT_VERSION = (10, 10, 0)
_MIN_GLASS_EFFECT_VERSION = (26, 0, 0)
HAS_VISUAL_EFFECT: bool = (
IS_MACOS
and MACOS_VERSION is not None
and MACOS_VERSION >= _MIN_VISUAL_EFFECT_VERSION
)
HAS_GLASS_EFFECT: bool = (
IS_MACOS
and MACOS_VERSION is not None
and MACOS_VERSION >= _MIN_GLASS_EFFECT_VERSION
)
class MacOSRequiredError(RuntimeError):
"""Raised when a macOS-only function is called on a non-macOS platform."""
def __init__(self, func_name: str) -> None:
super().__init__(f"{func_name} requires macOS")
def require_macos[**P, R](func: Callable[P, R]) -> Callable[P, R]:
"""
Decorator that raises an error if called on non-macOS platforms.
Args:
func: The function to wrap.
Returns:
A wrapped function that raises MacOSRequiredError on non-macOS.
Raises:
MacOSRequiredError: If called on a non-macOS platform.
"""
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
if not IS_MACOS:
raise MacOSRequiredError(func.__name__)
return func(*args, **kwargs)
return wrapper
def platform_guard[**P, R](func: Callable[P, R | None]) -> Callable[P, R | None]:
"""
Decorator that makes a function a no-op on non-macOS platforms.
The wrapped function will return None without executing on non-macOS
platforms, allowing cross-platform code to call macOS-specific
functions safely.
Args:
func: The function to wrap.
Returns:
A wrapped function that returns None on non-macOS platforms.
"""
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
if not IS_MACOS:
return None
return func(*args, **kwargs)
return wrapper

View File

@@ -0,0 +1,100 @@
"""Type definitions for pyqt-liquidglass."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import IntEnum
__all__ = ["BlendingMode", "GlassMaterial", "GlassOptions"]
class GlassMaterial(IntEnum):
"""
Available materials for glass effects.
These map to NSVisualEffectMaterial values for the fallback
implementation on pre-macOS 26 systems.
"""
TITLEBAR = 3
SELECTION = 4
MENU = 5
POPOVER = 6
SIDEBAR = 7
HEADER_VIEW = 10
SHEET = 11
WINDOW_BACKGROUND = 12
HUD = 13
FULLSCREEN_UI = 15
TOOLTIP = 17
CONTENT_BACKGROUND = 18
UNDER_WINDOW_BACKGROUND = 21
UNDER_PAGE_BACKGROUND = 22
class BlendingMode(IntEnum):
"""
Blending modes for visual effect views.
These map to NSVisualEffectBlendingMode values.
"""
BEHIND_WINDOW = 0
WITHIN_WINDOW = 1
@dataclass(frozen=True, slots=True)
class GlassOptions:
"""
Configuration options for glass effects.
Attributes:
corner_radius: Corner radius in points for rounded glass effects.
Only applies to NSGlassEffectView on macOS 26+.
material: The visual effect material to use. Only applies to
NSVisualEffectView fallback on older macOS versions.
blending_mode: How the effect blends with content. Only applies
to NSVisualEffectView fallback.
padding: Inset padding from widget edges in points (left, top, right, bottom).
"""
corner_radius: float = 0.0
material: GlassMaterial = GlassMaterial.UNDER_WINDOW_BACKGROUND
blending_mode: BlendingMode = BlendingMode.BEHIND_WINDOW
padding: tuple[float, float, float, float] = field(default=(0.0, 0.0, 0.0, 0.0))
@classmethod
def sidebar(
cls, *, corner_radius: float = 10.0, padding: float = 9.0
) -> GlassOptions:
"""
Create options optimized for sidebar glass effects.
Args:
corner_radius: Corner radius for rounded corners.
padding: Uniform padding from all edges.
Returns:
GlassOptions configured for sidebar use.
"""
return cls(
corner_radius=corner_radius,
material=GlassMaterial.SIDEBAR,
blending_mode=BlendingMode.BEHIND_WINDOW,
padding=(padding, padding, padding, padding),
)
@classmethod
def window(cls) -> GlassOptions:
"""
Create options for full window glass effects.
Returns:
GlassOptions configured for window-wide glass.
"""
return cls(
corner_radius=0.0,
material=GlassMaterial.UNDER_WINDOW_BACKGROUND,
blending_mode=BlendingMode.BEHIND_WINDOW,
padding=(0.0, 0.0, 0.0, 0.0),
)

View File

@@ -0,0 +1,334 @@
"""Core glass effect implementation for macOS Liquid Glass."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ._bridge import get_nsview_from_widget
from ._platform import HAS_GLASS_EFFECT, IS_MACOS, platform_guard
from ._types import BlendingMode, GlassOptions
if TYPE_CHECKING:
from ._compat import QtWidgets
__all__ = ["apply_glass_to_widget", "apply_glass_to_window", "remove_glass_effect"]
_effect_registry: dict[int, tuple[Any, Any]] = {}
_next_effect_id: int = 0
_NS_WINDOW_BELOW: int = -1
_NS_VIEW_WIDTH_SIZABLE: int = 2
_NS_VIEW_HEIGHT_SIZABLE: int = 16
_NS_VIEW_MAX_X_MARGIN: int = 4
_NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK: int = 1 << 15
def _create_glass_view(frame: Any, options: GlassOptions) -> Any | None: # noqa: ANN401
"""
Create an NSGlassEffectView or NSVisualEffectView.
Attempts to use NSGlassEffectView on macOS 26+, falling back to
NSVisualEffectView on older versions.
Args:
frame: NSRect for the view's frame.
options: Glass effect configuration.
Returns:
The created view, or None on failure.
"""
import objc # noqa: PLC0415 # ty: ignore
if HAS_GLASS_EFFECT:
try:
glass_cls = objc.lookUpClass("NSGlassEffectView") # ty: ignore
glass = glass_cls.alloc().initWithFrame_(frame) # ty: ignore
if options.corner_radius > 0:
glass.setCornerRadius_(options.corner_radius) # ty: ignore
except objc.nosuchclass_error: # ty: ignore
pass
else:
return glass
try:
from AppKit import ( # noqa: PLC0415 # ty: ignore
NSVisualEffectBlendingModeBehindWindow,
NSVisualEffectBlendingModeWithinWindow,
NSVisualEffectStateActive,
NSVisualEffectView,
)
glass = NSVisualEffectView.alloc().initWithFrame_(frame) # ty: ignore
glass.setMaterial_(options.material.value) # ty: ignore
blending = (
NSVisualEffectBlendingModeBehindWindow # ty: ignore
if options.blending_mode == BlendingMode.BEHIND_WINDOW
else NSVisualEffectBlendingModeWithinWindow # ty: ignore
)
glass.setBlendingMode_(blending) # ty: ignore
glass.setState_(NSVisualEffectStateActive) # ty: ignore
except Exception: # noqa: BLE001
return None
else:
return glass
def _configure_window_for_glass(ns_window: Any) -> None: # noqa: ANN401
"""Configure NSWindow properties for full window glass effect rendering."""
from AppKit import NSColor # noqa: PLC0415 # ty: ignore
ns_window.setOpaque_(False) # ty: ignore
ns_window.setBackgroundColor_(NSColor.clearColor()) # ty: ignore
current_mask = ns_window.styleMask() # ty: ignore
ns_window.setStyleMask_(current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK) # ty: ignore
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
def _configure_titlebar_for_glass(ns_window: Any) -> None: # noqa: ANN401
"""Configure only titlebar for widget-level glass (no window transparency)."""
current_mask = ns_window.styleMask() # ty: ignore
ns_window.setStyleMask_(current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK) # ty: ignore
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
@platform_guard
def apply_glass_to_window(
window: QtWidgets.QWidget, options: GlassOptions | None = None
) -> int | None:
"""
Apply glass effect to an entire window.
Creates an NSGlassEffectView (macOS 26+) or NSVisualEffectView (fallback)
that fills the window's content area behind all Qt content.
Uses one of three strategies based on window configuration:
1. Sibling Injection: If root view has a superview, adds glass as sibling
2. Content Swap: For frameless windows, creates a container and swaps
3. Child Fallback: Adds glass inside root view at bottom of z-order
Args:
window: A top-level QWidget (typically QMainWindow).
options: Glass effect configuration. Uses defaults if None.
Returns:
An effect ID for later removal, or None if the effect could not
be applied.
Note:
The window should be shown before calling this function.
"""
if options is None:
options = GlassOptions.window()
root_view = get_nsview_from_widget(window)
if root_view is None:
return None
ns_window = root_view.window() # ty: ignore
if ns_window is None:
return None
from AppKit import NSView # noqa: PLC0415 # ty: ignore
from Foundation import NSMakeRect # noqa: PLC0415 # ty: ignore
superview = root_view.superview() # ty: ignore
content_view = ns_window.contentView() # ty: ignore
container: Any = None
performed_swap = False
if superview is not None:
container = superview
elif root_view == content_view:
frame = root_view.frame() # ty: ignore
new_container = NSView.alloc().initWithFrame_(frame) # ty: ignore
new_container.setAutoresizingMask_( # ty: ignore
_NS_VIEW_WIDTH_SIZABLE | _NS_VIEW_HEIGHT_SIZABLE
)
new_container.setWantsLayer_(True) # ty: ignore
ns_window.setContentView_(new_container) # ty: ignore
root_view.setFrame_(new_container.bounds()) # ty: ignore
root_view.setAutoresizingMask_( # ty: ignore
_NS_VIEW_WIDTH_SIZABLE | _NS_VIEW_HEIGHT_SIZABLE
)
new_container.addSubview_(root_view) # ty: ignore
container = root_view.superview() # ty: ignore
performed_swap = True
else:
container = root_view
_configure_window_for_glass(ns_window)
if container == root_view.superview(): # ty: ignore
frame_rect = root_view.frame() # ty: ignore
else:
frame_rect = root_view.bounds() # ty: ignore
if performed_swap:
frame_rect = container.bounds() # ty: ignore
pad_left, pad_top, pad_right, pad_bottom = options.padding
frame_rect = NSMakeRect( # ty: ignore
frame_rect.origin.x + pad_left, # ty: ignore
frame_rect.origin.y + pad_bottom, # ty: ignore
frame_rect.size.width - pad_left - pad_right, # ty: ignore
frame_rect.size.height - pad_top - pad_bottom, # ty: ignore
)
glass = _create_glass_view(frame_rect, options)
if glass is None:
return None
glass.setAutoresizingMask_(_NS_VIEW_WIDTH_SIZABLE | _NS_VIEW_HEIGHT_SIZABLE) # ty: ignore
if container == root_view.superview(): # ty: ignore
container.addSubview_positioned_relativeTo_( # ty: ignore
glass, _NS_WINDOW_BELOW, root_view
)
else:
container.addSubview_positioned_relativeTo_( # ty: ignore
glass, _NS_WINDOW_BELOW, None
)
global _next_effect_id # noqa: PLW0603
effect_id = _next_effect_id
_next_effect_id += 1
_effect_registry[effect_id] = (glass, container)
window._glass_view = glass # type: ignore[attr-defined] # noqa: SLF001 # ty: ignore
return effect_id
@platform_guard
def apply_glass_to_widget(
widget: QtWidgets.QWidget, options: GlassOptions | None = None
) -> int | None:
"""
Apply glass effect to a specific widget.
Creates a glass effect view sized and positioned to match the widget's
geometry within its parent window.
Args:
widget: The widget to apply the glass effect to.
options: Glass effect configuration. Uses GlassOptions.sidebar()
defaults if None.
Returns:
An effect ID for later removal, or None if the effect could not
be applied.
Note:
The widget must be visible and part of a shown window.
The effect view tracks widget resize and move events.
"""
from ._compat import QtCore # noqa: PLC0415
if options is None:
options = GlassOptions.sidebar()
widget.setAttribute(QtCore.Qt.WidgetAttribute.WA_NativeWindow)
widget.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
ns_view = get_nsview_from_widget(widget)
if ns_view is None:
return None
ns_window = ns_view.window() # ty: ignore
if ns_window is None:
return None
superview = ns_view.superview() # ty: ignore
if superview is None:
return None
from Foundation import NSMakeRect # noqa: PLC0415 # ty: ignore
_configure_titlebar_for_glass(ns_window)
content_view = ns_window.contentView() # ty: ignore
sidebar_frame = ns_view.frame() # ty: ignore
content_frame = content_view.frame() # ty: ignore
pad_left, pad_top, pad_right, pad_bottom = options.padding
glass_frame = NSMakeRect( # ty: ignore
sidebar_frame.origin.x + pad_left, # ty: ignore
pad_bottom,
sidebar_frame.size.width - pad_left - pad_right, # ty: ignore
content_frame.size.height - pad_top - pad_bottom, # ty: ignore
)
glass = _create_glass_view(glass_frame, options)
if glass is None:
return None
glass.setAutoresizingMask_(_NS_VIEW_HEIGHT_SIZABLE | _NS_VIEW_MAX_X_MARGIN) # ty: ignore
content_view.addSubview_positioned_relativeTo_(glass, _NS_WINDOW_BELOW, None) # ty: ignore
def update_glass_frame() -> None:
sf = ns_view.frame() # ty: ignore
cf = content_view.frame() # ty: ignore
new_frame = NSMakeRect( # ty: ignore
sf.origin.x + pad_left, # ty: ignore
pad_bottom,
sf.size.width - pad_left - pad_right, # ty: ignore
cf.size.height - pad_top - pad_bottom, # ty: ignore
)
glass.setFrame_(new_frame) # ty: ignore
original_resize = widget.resizeEvent
original_move = widget.moveEvent
def on_resize(event: Any) -> None: # noqa: ANN401
update_glass_frame()
original_resize(event)
def on_move(event: Any) -> None: # noqa: ANN401
update_glass_frame()
original_move(event)
widget.resizeEvent = on_resize # type: ignore[method-assign]
widget.moveEvent = on_move # type: ignore[method-assign]
global _next_effect_id # noqa: PLW0603
effect_id = _next_effect_id
_next_effect_id += 1
_effect_registry[effect_id] = (glass, content_view)
widget._glass_view = glass # type: ignore[attr-defined] # noqa: SLF001
widget._update_glass_frame = update_glass_frame # type: ignore[attr-defined] # noqa: SLF001
return effect_id
def remove_glass_effect(effect_id: int) -> bool:
"""
Remove a previously applied glass effect.
Args:
effect_id: The identifier returned by apply_glass_to_window or
apply_glass_to_widget.
Returns:
True if the effect was successfully removed, False if the effect
ID was not found.
"""
if effect_id not in _effect_registry:
return False
glass_view, _ = _effect_registry.pop(effect_id)
if IS_MACOS:
try:
glass_view.removeFromSuperview() # ty: ignore
except Exception: # noqa: BLE001
return False
return True

View File

@@ -0,0 +1,133 @@
"""Helper functions for preparing Qt widgets for glass effects."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ._bridge import get_nswindow_from_widget
from ._platform import IS_MACOS
if TYPE_CHECKING:
from ._compat import QtWidgets
__all__ = [
"prepare_widget_for_glass",
"prepare_window_for_glass",
"set_window_background_transparent",
]
_NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK: int = 1 << 15
_NS_WINDOW_TITLE_HIDDEN: int = 1
_NS_WINDOW_STYLE_MASK_BORDERLESS: int = 0
def prepare_window_for_glass(
window: QtWidgets.QWidget,
*,
frameless: bool = False,
transparent_titlebar: bool = True,
full_size_content: bool = True,
) -> None:
"""
Prepare a window for glass effects.
Sets the necessary Qt widget attributes and configures the native
NSWindow properties for glass effect rendering.
Args:
window: The window widget to prepare.
frameless: If True, remove the window frame entirely using
Qt.FramelessWindowHint.
transparent_titlebar: If True, make the titlebar transparent
on macOS so glass can extend underneath.
full_size_content: If True, extend content view to cover the
titlebar area.
Note:
Call this before showing the window for best results.
"""
from ._compat import QtCore # noqa: PLC0415
window.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
if frameless:
window.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint)
if not IS_MACOS:
return
window.show()
ns_window = get_nswindow_from_widget(window)
if ns_window is None:
return
if frameless:
ns_window.setHasShadow_(False) # ty: ignore # Keep shadow for depth
return
if full_size_content:
current_mask = ns_window.styleMask() # ty: ignore
ns_window.setStyleMask_( # ty: ignore
current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK
)
if transparent_titlebar:
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
ns_window.setTitleVisibility_(_NS_WINDOW_TITLE_HIDDEN) # ty: ignore
def prepare_widget_for_glass(widget: QtWidgets.QWidget) -> None:
"""
Prepare a widget for having glass effect applied.
Sets the necessary Qt attributes for the widget to render correctly
with a glass effect behind it. The widget's content will be visible
on top of the glass effect.
Args:
widget: The widget to prepare.
Note:
This sets WA_TranslucentBackground which makes the widget
background transparent. Ensure your widget's stylesheet or
paint event handles the transparent background appropriately.
"""
from ._compat import QtCore # noqa: PLC0415
widget.setAttribute(QtCore.Qt.WidgetAttribute.WA_NativeWindow)
widget.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
def set_window_background_transparent(window: QtWidgets.QWidget) -> None:
"""
Make a window's background fully transparent.
This is useful when you want complete control over the window
appearance, such as creating a fully custom-drawn window with
glass effects.
Args:
window: The window to make transparent.
Note:
After calling this, the window will have no visible background.
You must provide your own background through stylesheets or
painting.
"""
from ._compat import QtCore, QtGui # noqa: PLC0415
window.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
window.setAttribute(QtCore.Qt.WidgetAttribute.WA_NoSystemBackground)
palette = window.palette()
palette.setColor(QtGui.QPalette.ColorRole.Window, QtGui.QColor(0, 0, 0, 0))
window.setPalette(palette)
if IS_MACOS:
ns_window = get_nswindow_from_widget(window)
if ns_window is not None:
from AppKit import NSColor # noqa: PLC0415 # ty: ignore
ns_window.setOpaque_(False) # ty: ignore
ns_window.setBackgroundColor_(NSColor.clearColor()) # ty: ignore

View File

View File

@@ -0,0 +1,223 @@
"""Traffic light (window button) positioning for macOS windows."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ._bridge import get_nsview_from_widget
from ._platform import platform_guard
if TYPE_CHECKING:
from ._compat import QtWidgets
__all__ = ["hide_traffic_lights", "setup_traffic_lights_inset", "show_traffic_lights"]
_NS_WINDOW_CLOSE_BUTTON: int = 0
_NS_WINDOW_MINIATURIZE_BUTTON: int = 1
_NS_WINDOW_ZOOM_BUTTON: int = 2
_NS_WINDOW_TITLE_HIDDEN: int = 1
_NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK: int = 1 << 15
_NS_LAYOUT_ATTRIBUTE_LEADING: int = 5
_NS_LAYOUT_ATTRIBUTE_TRAILING: int = 6
_NS_LAYOUT_ATTRIBUTE_CENTER_Y: int = 10
_NS_LAYOUT_RELATION_EQUAL: int = 0
def _get_traffic_light_buttons(ns_window: Any) -> tuple[Any, Any, Any]: # noqa: ANN401
"""Get the close, minimize, and zoom buttons from an NSWindow."""
close_btn = ns_window.standardWindowButton_(_NS_WINDOW_CLOSE_BUTTON) # ty: ignore
minimize_btn = ns_window.standardWindowButton_(_NS_WINDOW_MINIATURIZE_BUTTON) # ty: ignore
zoom_btn = ns_window.standardWindowButton_(_NS_WINDOW_ZOOM_BUTTON) # ty: ignore
return close_btn, minimize_btn, zoom_btn
@platform_guard
def setup_traffic_lights_inset(
window: QtWidgets.QWidget, x_offset: float = 0.0, y_offset: float = 0.0
) -> bool:
"""
Reposition the traffic light buttons (close, minimize, zoom).
Uses NSLayoutConstraint to position the buttons with an offset from
their default location. This method is more robust than frame-based
positioning as it survives window resizes.
Args:
window: The window whose traffic lights to reposition.
x_offset: Horizontal offset in points from the left edge.
y_offset: Vertical offset in points from the center.
Returns:
True if the traffic lights were successfully repositioned,
False otherwise.
Note:
This function configures the window for full-size content view
and transparent titlebar automatically.
"""
ns_view = get_nsview_from_widget(window)
if ns_view is None:
return False
ns_window = ns_view.window() # ty: ignore
if ns_window is None:
return False
from AppKit import NSLayoutConstraint # noqa: PLC0415 # ty: ignore
current_mask = ns_window.styleMask() # ty: ignore
ns_window.setStyleMask_( # ty: ignore
current_mask | _NS_FULL_SIZE_CONTENT_VIEW_WINDOW_MASK
)
ns_window.setTitlebarAppearsTransparent_(True) # ty: ignore
ns_window.setTitleVisibility_(_NS_WINDOW_TITLE_HIDDEN) # ty: ignore
close_btn, minimize_btn, zoom_btn = _get_traffic_light_buttons(ns_window)
if close_btn is None:
return False
close_btn.setTranslatesAutoresizingMaskIntoConstraints_(False) # ty: ignore
if minimize_btn is not None:
minimize_btn.setTranslatesAutoresizingMaskIntoConstraints_(False) # ty: ignore
if zoom_btn is not None:
zoom_btn.setTranslatesAutoresizingMaskIntoConstraints_(False) # ty: ignore
superview = close_btn.superview() # ty: ignore
if superview is None:
return False
button_spacing = 6.0
make_constraint = ( # ty: ignore
NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_
)
constraint_close_x = make_constraint(
close_btn,
_NS_LAYOUT_ATTRIBUTE_LEADING,
_NS_LAYOUT_RELATION_EQUAL,
superview,
_NS_LAYOUT_ATTRIBUTE_LEADING,
1.0,
x_offset,
)
superview.addConstraint_(constraint_close_x) # ty: ignore
constraint_close_y = make_constraint(
close_btn,
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
_NS_LAYOUT_RELATION_EQUAL,
superview,
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
1.0,
y_offset,
)
superview.addConstraint_(constraint_close_y) # ty: ignore
if minimize_btn is not None:
constraint_min_x = make_constraint(
minimize_btn,
_NS_LAYOUT_ATTRIBUTE_LEADING,
_NS_LAYOUT_RELATION_EQUAL,
close_btn,
_NS_LAYOUT_ATTRIBUTE_TRAILING,
1.0,
button_spacing,
)
superview.addConstraint_(constraint_min_x) # ty: ignore
constraint_min_y = make_constraint(
minimize_btn,
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
_NS_LAYOUT_RELATION_EQUAL,
superview,
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
1.0,
y_offset,
)
superview.addConstraint_(constraint_min_y) # ty: ignore
if zoom_btn is not None and minimize_btn is not None:
constraint_zoom_x = make_constraint(
zoom_btn,
_NS_LAYOUT_ATTRIBUTE_LEADING,
_NS_LAYOUT_RELATION_EQUAL,
minimize_btn,
_NS_LAYOUT_ATTRIBUTE_TRAILING,
1.0,
button_spacing,
)
superview.addConstraint_(constraint_zoom_x) # ty: ignore
constraint_zoom_y = make_constraint(
zoom_btn,
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
_NS_LAYOUT_RELATION_EQUAL,
superview,
_NS_LAYOUT_ATTRIBUTE_CENTER_Y,
1.0,
y_offset,
)
superview.addConstraint_(constraint_zoom_y) # ty: ignore
return True
@platform_guard
def hide_traffic_lights(window: QtWidgets.QWidget) -> bool:
"""
Hide the traffic light buttons while keeping window functionality.
The buttons are hidden but the window remains closable, minimizable,
and zoomable via keyboard shortcuts and menu commands.
Args:
window: The window whose traffic lights to hide.
Returns:
True if successful, False otherwise.
"""
ns_view = get_nsview_from_widget(window)
if ns_view is None:
return False
ns_window = ns_view.window() # ty: ignore
if ns_window is None:
return False
close_btn, minimize_btn, zoom_btn = _get_traffic_light_buttons(ns_window)
for btn in (close_btn, minimize_btn, zoom_btn):
if btn is not None:
btn.setHidden_(True) # ty: ignore
return True
@platform_guard
def show_traffic_lights(window: QtWidgets.QWidget) -> bool:
"""
Show previously hidden traffic light buttons.
Args:
window: The window whose traffic lights to show.
Returns:
True if successful, False otherwise.
"""
ns_view = get_nsview_from_widget(window)
if ns_view is None:
return False
ns_window = ns_view.window() # ty: ignore
if ns_window is None:
return False
close_btn, minimize_btn, zoom_btn = _get_traffic_light_buttons(ns_window)
for btn in (close_btn, minimize_btn, zoom_btn):
if btn is not None:
btn.setHidden_(False) # ty: ignore
return True

6
ty.toml Normal file
View File

@@ -0,0 +1,6 @@
[environment]
python = ".venv"
[rules]
unresolved-import = "ignore"
unresolved-attribute = "ignore"

241
uv.lock generated Normal file
View File

@@ -0,0 +1,241 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "librt"
version = "0.7.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" },
{ url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" },
{ url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" },
{ url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" },
{ url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" },
{ url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" },
{ url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" },
{ url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" },
{ url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" },
{ url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" },
{ url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" },
{ url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" },
{ url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" },
{ url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" },
{ url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" },
{ url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" },
{ url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" },
{ url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" },
{ url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" },
{ url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" },
{ url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" },
{ url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" },
{ url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" },
{ url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" },
{ url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" },
{ url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" },
{ url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" },
{ url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" },
{ url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" },
{ url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
]
[[package]]
name = "mypy"
version = "1.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
{ url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
{ url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
{ url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
{ url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
{ url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
{ url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
{ url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
{ url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
{ url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "pathspec"
version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
]
[[package]]
name = "pyobjc-core"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" },
{ url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" },
{ url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" },
{ url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" },
]
[[package]]
name = "pyobjc-framework-cocoa"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" },
{ url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" },
{ url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" },
]
[[package]]
name = "pyobjc-framework-quartz"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" },
{ url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" },
{ url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" },
]
[[package]]
name = "pyqt-liquidglass"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "pyobjc-framework-cocoa" },
{ name = "pyobjc-framework-quartz" },
]
[package.dev-dependencies]
dev = [
{ name = "pyside6-essentials" },
{ name = "pyside6-stubs" },
]
[package.metadata]
requires-dist = [
{ name = "pyobjc-framework-cocoa", specifier = ">=12.1" },
{ name = "pyobjc-framework-quartz", specifier = ">=12.1" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pyside6-essentials", specifier = ">=6.10.1" },
{ name = "pyside6-stubs", specifier = ">=6.7.3.0" },
]
[[package]]
name = "pyside6"
version = "6.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyside6-addons" },
{ name = "pyside6-essentials" },
{ name = "shiboken6" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/22/f82cfcd1158be502c5741fe67c3fa853f3c1edbd3ac2c2250769dd9722d1/pyside6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:d0e70dd0e126d01986f357c2a555722f9462cf8a942bf2ce180baf69f468e516", size = 558169, upload-time = "2025-11-20T10:09:08.79Z" },
{ url = "https://files.pythonhosted.org/packages/66/eb/54afe242a25d1c33b04ecd8321a549d9efb7b89eef7690eed92e98ba1dc9/pyside6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4053bf51ba2c2cb20e1005edd469997976a02cec009f7c46356a0b65c137f1fa", size = 557818, upload-time = "2025-11-20T10:09:10.132Z" },
{ url = "https://files.pythonhosted.org/packages/4d/af/5706b1b33587dc2f3dfa3a5000424befba35e4f2d5889284eebbde37138b/pyside6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7d3ca20a40139ca5324a7864f1d91cdf2ff237e11bd16354a42670f2a4eeb13c", size = 558358, upload-time = "2025-11-20T10:09:11.288Z" },
{ url = "https://files.pythonhosted.org/packages/26/41/3f48d724ecc8e42cea8a8442aa9b5a86d394b85093275990038fd1020039/pyside6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9f89ff994f774420eaa38cec6422fddd5356611d8481774820befd6f3bb84c9e", size = 564424, upload-time = "2025-11-20T10:09:12.677Z" },
{ url = "https://files.pythonhosted.org/packages/af/30/395411473b433875a82f6b5fdd0cb28f19a0e345bcaac9fbc039400d7072/pyside6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:9c5c1d94387d1a32a6fae25348097918ef413b87dfa3767c46f737c6d48ae437", size = 548866, upload-time = "2025-11-20T10:09:14.174Z" },
]
[[package]]
name = "pyside6-addons"
version = "6.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyside6-essentials" },
{ name = "shiboken6" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/f9/b72a2578d7dbef7741bb90b5756b4ef9c99a5b40148ea53ce7f048573fe9/pyside6_addons-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4d2b82bbf9b861134845803837011e5f9ac7d33661b216805273cf0c6d0f8e82", size = 322639446, upload-time = "2025-11-20T09:54:50.75Z" },
{ url = "https://files.pythonhosted.org/packages/94/3b/3ed951c570a15570706a89d39bfd4eaaffdf16d5c2dca17e82fc3ec8aaa6/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:330c229b58d30083a7b99ed22e118eb4f4126408429816a4044ccd0438ae81b4", size = 170678293, upload-time = "2025-11-20T09:56:40.991Z" },
{ url = "https://files.pythonhosted.org/packages/22/77/4c780b204d0bf3323a75c184e349d063e208db44c993f1214aa4745d6f47/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:56864b5fecd6924187a2d0f7e98d968ed72b6cc267caa5b294cd7e88fff4e54c", size = 166365011, upload-time = "2025-11-20T09:57:20.261Z" },
{ url = "https://files.pythonhosted.org/packages/04/14/58239776499e6b279fa6ca2e0d47209531454b99f6bd2ad7c96f11109416/pyside6_addons-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:b6e249d15407dd33d6a2ffabd9dc6d7a8ab8c95d05f16a71dad4d07781c76341", size = 164864664, upload-time = "2025-11-20T09:57:54.815Z" },
{ url = "https://files.pythonhosted.org/packages/e2/cd/1b74108671ba4b1ebb2661330665c4898b089e9c87f7ba69fe2438f3d1b6/pyside6_addons-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:0de303c0447326cdc6c8be5ab066ef581e2d0baf22560c9362d41b8304fdf2db", size = 34191225, upload-time = "2025-11-20T09:58:04.184Z" },
]
[[package]]
name = "pyside6-essentials"
version = "6.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "shiboken6" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/b0/c43209fecef79912e9b1c70a1b5172b1edf76caebcc885c58c60a09613b0/pyside6_essentials-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:cd224aff3bb26ff1fca32c050e1c4d0bd9f951a96219d40d5f3d0128485b0bbe", size = 105461499, upload-time = "2025-11-20T09:59:23.733Z" },
{ url = "https://files.pythonhosted.org/packages/5f/8e/b69ba7fa0c701f3f4136b50460441697ec49ee6ea35c229eb2a5ee4b5952/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e9ccbfb58c03911a0bce1f2198605b02d4b5ca6276bfc0cbcf7c6f6393ffb856", size = 76764617, upload-time = "2025-11-20T09:59:38.831Z" },
{ url = "https://files.pythonhosted.org/packages/bd/83/569d27f4b6c6b9377150fe1a3745d64d02614021bea233636bc936a23423/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ec8617c9b143b0c19ba1cc5a7e98c538e4143795480cb152aee47802c18dc5d2", size = 75850373, upload-time = "2025-11-20T09:59:56.082Z" },
{ url = "https://files.pythonhosted.org/packages/1e/64/a8df6333de8ccbf3a320e1346ca30d0f314840aff5e3db9b4b66bf38e26c/pyside6_essentials-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9555a48e8f0acf63fc6a23c250808db841b28a66ed6ad89ee0e4df7628752674", size = 74491180, upload-time = "2025-11-20T10:00:11.215Z" },
{ url = "https://files.pythonhosted.org/packages/67/da/65cc6c6a870d4ea908c59b2f0f9e2cf3bfc6c0710ebf278ed72f69865e4e/pyside6_essentials-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:4d1d248644f1778f8ddae5da714ca0f5a150a5e6f602af2765a7d21b876da05c", size = 55190458, upload-time = "2025-11-20T10:00:26.226Z" },
]
[[package]]
name = "pyside6-stubs"
version = "6.7.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy" },
{ name = "pyside6" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/2b/d2a3fdb212475682b11e8b6330f2cada60dcbbaf151f73f86844b11d1db6/pyside6_stubs-6.7.3.0.tar.gz", hash = "sha256:db8def2bb9c74091bfa2a4df7610d3ea344ecd00ebd4283995eeb5fda8383603", size = 509931, upload-time = "2025-03-04T05:49:15.755Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/2e/3488fa3baca56ff494b9502424675f5b14b11e966eb74d5adaf1811cd651/pyside6_stubs-6.7.3.0-py3-none-any.whl", hash = "sha256:7a2ef0d7486939f240e745829f80887ec3eab280e52c9930d64b48b3de95042a", size = 550616, upload-time = "2025-03-04T05:49:13.154Z" },
]
[[package]]
name = "shiboken6"
version = "6.10.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/8b/e5db743d505ceea3efc4cd9634a3bee22a3e2bf6e07cefd28c9b9edabcc6/shiboken6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:9f2990f5b61b0b68ecadcd896ab4441f2cb097eef7797ecc40584107d9850d71", size = 478483, upload-time = "2025-11-20T10:08:52.411Z" },
{ url = "https://files.pythonhosted.org/packages/56/ba/b50c1a44b3c4643f482afbf1a0ea58f393827307100389ce29404f9ad3b0/shiboken6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4221a52dfb81f24a0d20cc4f8981cb6edd810d5a9fb28287ce10d342573a0e4", size = 271993, upload-time = "2025-11-20T10:08:54.093Z" },
{ url = "https://files.pythonhosted.org/packages/16/b8/939c24ebd662b0aa5c945443d0973145b3fb7079f0196274ef7bb4b98f73/shiboken6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c095b00f4d6bf578c0b2464bb4e264b351a99345374478570f69e2e679a2a1d0", size = 268691, upload-time = "2025-11-20T10:08:55.639Z" },
{ url = "https://files.pythonhosted.org/packages/cf/a6/8c65ee0fa5e172ebcca03246b1bc3bd96cdaf1d60537316648536b7072a5/shiboken6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:c1601d3cda1fa32779b141663873741b54e797cb0328458d7466281f117b0a4e", size = 1234704, upload-time = "2025-11-20T10:08:57.417Z" },
{ url = "https://files.pythonhosted.org/packages/7b/6a/c0fea2f2ac7d9d96618c98156500683a4d1f93fea0e8c5a2bc39913d7ef1/shiboken6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:5cf800917008587b551005a45add2d485cca66f5f7ecd5b320e9954e40448cc9", size = 1795567, upload-time = "2025-11-20T10:08:59.184Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]