Skip to main content

Physically-grounded refractive glass widgets for PyQt6 — real refraction, chromatic dispersion, Fresnel reflection and frost.

Project description

PyGlass

Apple-style liquid glass for PyQt6 — real refraction, on macOS and Windows.

PyPI Python License: MIT Stars

PyGlass refracting the live desktop

I couldn't sleep one night, so I sat down with Claude Code and tried to replicate Apple's "liquid glass" — for real, in a plain PyQt6 + numpy stack that runs the same on macOS and Windows.

At my company we ship B2B software, and like a lot of B2B software it looked like it was frozen in 2010. My customers aren't digital natives — but they stare at our screens every single day, and I think they deserve something that feels alive too, not the same tired status quo. This is me trying to give a little of that back.

It's MIT-licensed — use it, build something nice with it. And if it made you smile, a ⭐ would genuinely make my day.

Not faux "glassmorphism" (a translucent white rectangle). PyGlass models the panel as a real beveled glass slab and refracts the pixels behind it: Snell lens-wrap at the rim, per-wavelength chromatic dispersion, Fresnel reflection, and an optional frosted blur — all in numpy, no shaders, no native code.

Install

pip install pyglass-qt          # from PyPI
# or straight from GitHub:
pip install "git+https://github.com/neomosh8/pyglass.git"

The distribution is pyglass-qt (the name pyglass was already taken on PyPI), but you still import pyglass. Only PyQt6 + numpy are pulled in.

Quick start

from pyglass import GlassPane, GlassMaterial
from PyQt6.QtWidgets import QVBoxLayout, QLabel

# A glass modal over your existing window — refracts whatever's behind it.
pane = GlassPane(my_window, material=GlassMaterial(thickness=0.6, frost=0.3))
QVBoxLayout(pane.content).addWidget(QLabel("Hello from glass"))
pane.show()

# …or, parentless, a frameless window that refracts the live desktop:
GlassPane(material=GlassMaterial(thickness=0.7, frost=0.15)).show()

Put your widgets in pane.content. A child pane captures its parent (with itself hidden) for the backdrop, so no cooperation from the host is needed — it works on any widget. Drag it anywhere; [ ] adjust thickness, - + adjust frost, Esc closes.

Gallery

In-app modal The frosted demo popup
glass modal over an app frosted refractive popup

A glass card refracting a live, animated scene (see examples/recipes.py):

live glass over a moving scene

The two dials

The whole look is driven by GlassMaterial — two perceptual dials in [0, 1] that re-derive a dozen physical parameters at once, so the pane always reads as one coherent piece of glass. The neutral pair (thickness=0.5, frost=0) reproduces the tuned baseline exactly.

Dial Meaning What it drives
thickness perceived slab depth / mass (optical path length) background displacement, the curved lens-wrap width, the IOR range / rim bend, chromatic dispersion, the spectral rim-line, and the capture margin so the wrap never clamps
frost surface roughness (ground / milk glass) a transmission blur (forward scatter), a milky multiple-scatter veil, and a softened dispersion line — transmission-side only, so frost=0 is byte-for-byte the sharp look

thickness is one scalar standing in for T: a thicker slab bends light more, has a bigger rounded edge, disperses colour more (longer optical path) and casts a thicker rim — all slaved together. frost is microfacet roughness: a rough face scatters transmitted light into a cone that projects to a blur.

GlassStyle separately tunes the non-physical chrome (shadow, tint, sheen, rim).

Two layers

High-level — GlassPane (above): a drop-in frameless glass widget. Child → in-app modal/panel; parentless → a top-level window over the desktop.

Low-level — compose it yourself inside any paintEvent:

from pyglass import GlassRenderer, paint_glass, WidgetBackdrop, GlassMaterial

backdrop = WidgetBackdrop(host)                 # or ScreenBackdrop(window)
renderer = GlassRenderer(GlassMaterial(), w, h, radius)
backdrop.changed.connect(self.update)
# in paintEvent:
pm = renderer.refract(backdrop.array(), origin, backdrop.dpr())
paint_glass(painter, panel_rect, radius, pm)

See pyglass/glass.py (GlassPopup) for a full worked example with a scrim and an open/close animation.

Recipes — embedding it well

examples/recipes.py is a runnable, heavily-commented guide to making glass feel right in a real app:

  • Live refraction of changing contentGlassPane grabs its backdrop on show / drag-release; if the content behind keeps moving (animation, video, a scrolling view), drive pane.refresh() on a timer so the glass tracks it.
  • Content-friendly material — use a thin bevel so the refracting rim doesn't bleed into your text; the interior stays a clean 1:1 surface.
  • Thick vs. frosted — clear thick glass (high thickness, low frost) reads as a block you see through; raise frost for a ground-glass look.
  • Legibility tint via GlassStyle, and free dragging that re-slices the cached backdrop as you move (full-quality on release).

Run the demos

python main.py            # the frosted refractive modal (above)
python main.py --desktop  # a glass window over your live desktop
python examples/in_app_modal.py
python examples/recipes.py

Desktop mode — glass over your real screen

A parentless GlassPane (or python main.py --desktop) floats over your live desktop and refracts whatever is behind it — all your windows, not just the wallpaper (that's the hero shot up top).

The pane keeps the glass out of its own capture (so it doesn't refract itself), which lets it re-grab the live screen without hiding — live, no flicker, and dragging re-slices the last grab each frame.

  • Windows: the Magnification API (MagSetWindowFilterList + MW_FILTERMODE_EXCLUDE) captures the screen with the glass filtered out of only this capture. So it's live and fully recordable — the window stays visible to Snipping Tool / OBS / Teams — with no flicker and no trade-off. Captures hardware-accelerated windows too. (Falls back to WDA_EXCLUDEFROMCAPTURE on pre-2004 Windows; see the toggle note below.)

  • macOS: shells out to the system screencapture (which, unlike Qt's grabWindow, returns the full screen with every window) and excludes itself via NSWindowSharingNone — live and flicker-free.

    Needs Screen Recording permission (System Settings → Privacy & Security → Screen Recording). The macOS exclusion is global, so the window is hidden from other recorders; press C to make it capturable (paused) and back. (A ScreenCaptureKit backend for live-and-recordable on macOS is on the list.)

  • Linux: no portable self-exclusion, so it captures once and stays paused (press R to refresh) — no flicker. The dials still work live.

C — capture toggle (macOS / Windows-fallback only): the global WDA/NSWindowSharingNone exclusion hides the window from all capture, so C drops it (window becomes recordable but pausedR to refresh) and toggles back to hidden + live. On Windows the Magnification path needs none of this — it's recordable while live.

Platform support

Cross-platform — macOS, Windows, Linux. PyQt6 + numpy only. The in-app glass reads the app's own rendered scene (no OS screen-capture permission needed); fonts fall back gracefully and device-pixel-ratio is handled, so it renders correctly on Windows HiDPI and Retina alike.

How the refraction works

The panel is a beveled glass slab over a rounded-rectangle signed distance field. The flat centre passes light straight through; the rim is a quarter-circle roundover whose slope grows toward the edge. The vertical incident ray is refracted there with Snell's law and projected through the glass thickness, so the 1/(-T_z) term curls the background into a curved lens-wrap (not a flat shift). Each colour channel uses its own IOR → a chromatic-dispersion fringe. The Schlick–Fresnel term rises from ~F0 at the centre to ~1 at the grazing rim, where the surface reflects a virtual environment. A lightened iridescent spectral line is added along the border. frost adds a fast separable blur of the transmitted background.

All geometry-dependent work (normals, per-channel sample coordinates, Fresnel weight, reflected environment) is precomputed once into a GlassKernel; each frame only runs the bilinear gather (+ the box blur when frosted), so dragging stays smooth.

Layout

File Purpose
pyglass/refract.py Engine — GlassKernel (refraction + Fresnel over a beveled SDF) and GlassMaterial (the two dials)
pyglass/effect.py GlassRenderer, paint_glass, GlassStyle — the reusable rendering core
pyglass/backdrop.py WidgetBackdrop / ScreenBackdropwhat the glass refracts
pyglass/pane.py GlassPane — the drop-in glass widget
pyglass/glass.py GlassPopup — in-app modal demo built on the low-level core
pyglass/desktop.py DesktopGlass — desktop-window demo
examples/ in_app_modal.py, desktop_window.py, recipes.py

License

MIT — see LICENSE. Built one sleepless night with the help of Claude Code. If you ship something nice with it, I'd love a ⭐.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyglass_qt-0.2.1.tar.gz (39.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyglass_qt-0.2.1-py3-none-any.whl (40.4 kB view details)

Uploaded Python 3

File details

Details for the file pyglass_qt-0.2.1.tar.gz.

File metadata

  • Download URL: pyglass_qt-0.2.1.tar.gz
  • Upload date:
  • Size: 39.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for pyglass_qt-0.2.1.tar.gz
Algorithm Hash digest
SHA256 869927b556643a8cf4ad5c3f0aff90357cdc9defffdaa4c1040a0c26771b5638
MD5 079aa8671c6b3550098d4b856a7c4c8b
BLAKE2b-256 2766083a7b17333ba313d9dbc7cbb34fc83219d4e7322aab824037ff28375b74

See more details on using hashes here.

File details

Details for the file pyglass_qt-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: pyglass_qt-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 40.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for pyglass_qt-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 18fc44a98775137ffb9d58f40b1755753a345b6102d7dfac78d034ea5c18f42e
MD5 38b8a0fcfff6314670b77b6be8aae43a
BLAKE2b-256 8a3222e219493cd5da411c8e1b166ba21241125e7cdde083d6f5ce1981c8a29c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page