Metadata-Version: 2.4
Name: medusa-style
Version: 0.3.0
Summary: Single source of truth for MEDUSA styling: one Style x Theme look for PySide6 QSS + Matplotlib, organized by rendering environment.
Project-URL: Homepage, https://www.medusabci.com/
Project-URL: Source, https://github.com/medusabci/medusa-style
Author: MEDUSA
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: bci,eeg,matplotlib,neuroscience,pyside6,qss,theming
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Software Development :: User Interfaces
Requires-Python: >=3.11
Requires-Dist: matplotlib>=3.6
Requires-Dist: pyside6>=6.5
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: numpy; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# medusa-style

**Single source of truth (SSOT) for MEDUSA styling.** One look themes BOTH
PySide6 desktop apps (via generated QSS) and Matplotlib scientific plots (built
on Matplotlib's bundled seaborn baseline) — so a microvolt is the same color in
the dark acquisition app and on a white-paper export, and an embedded plot is
seamless with the panel that hosts it.

The look has **two independent axes**:

- a **theme** — the *color scheme* (`dark`, `light`, …), a `Palette`.
- a **style** — the *visual language* (`modern`, `presentation`, …): fonts,
  density and plot line widths, a `Style`.

Any style renders in any theme. The code is organized by **rendering
environment** (`qt/`, `mpl/`, …), so adding a backend never touches the others.

## Install

```bash
pip install git+https://github.com/medusabci/medusa-style.git
```

That's all — PySide6 and Matplotlib are both core dependencies (every MEDUSA
component needs both). The only extra is `[dev]` for building/testing the
package itself. The seaborn *library* is **not** required: the seaborn base
styles ship inside Matplotlib.

## The whole API

```python
import medusa_style as ms

ms.apply(app)               # paint a Qt application (widgets + seamless plots)
ms.apply(widget)            # scope the theme to one Qt widget
ms.apply()                  # theme every Matplotlib figure you draw
ms.apply(fig)               # restyle one figure in place
ms.apply(ax)                # restyle one axes in place
ms.use_theme("light")       # switch COLORS — restyles everything already styled
ms.use_style("presentation")# switch VISUAL LANGUAGE — restyles everything
ms.current_theme()          # the active Palette
ms.current_style()          # the active Style
ms.themes()                 # ['dark', 'light']          — for a theme picker
ms.styles()                 # ['modern', 'presentation'] — for a style picker
ms.customize(colors=..., style=...)   # one-line house theme / style
ms.on_change(cb)            # react to switches — cb(palette, style)
ms.categorical_color(7)     # SSOT categorical color #7
```

`apply()` dispatches on what you give it: nothing → global Matplotlib, a
`QApplication`/`QWidget` → Qt, a `Figure`/`Axes` → Matplotlib. You never choose a
backend. Whatever you pass is remembered, so a later `use_theme()`/`use_style()`
repaints it with no loop on your side.

### Desktop app (PySide6)

```python
import sys
import medusa_style as ms
from medusa_style.qt import application, PlotPanel
from PySide6.QtWidgets import QMainWindow

app = application(sys.argv)        # Hi-DPI handled before construction; themed
ms.apply()                         # theme Matplotlib figures too

win = QMainWindow()
panel = PlotPanel(toolbar=True)    # seamless embed — no objectName, no restyle call
panel.figure.add_subplot(111).plot(range(10), color=ms.categorical_color(0))
win.setCentralWidget(panel)
win.show()

# View menu -> app AND embedded figure restyle, one line each:
view = win.menuBar().addMenu("View")
view.addAction("Light", lambda: ms.use_theme("light"))
view.addAction("Presentation", lambda: ms.use_style("presentation"))
sys.exit(app.exec())
```

`PlotPanel` is the borderless Matplotlib↔Qt seam as a drop-in widget: its
background equals the plot's, and it tracks itself for live theme/style switches.
For a frame you already own, use `medusa_style.qt.embed_figure(frame, fig)`.

### Notebook / headless (Qt never imported)

```python
import medusa_style as ms
ms.apply()                  # theme Matplotlib globally
fig = make_figure()
ms.apply(fig)               # ...or restyle one figure
ms.use_theme("dark")        # restyles tracked figures in place
```

Importing `medusa_style` pulls in **neither** PySide6 **nor** Matplotlib until
you actually style something.

## Themes and styles

Two built-in **themes** (color schemes), both tuned for long medical/scientific
sessions:

- **dark** (default) — deep, cool, near-black canvas for dim-lab real-time use.
- **light** — a calm cool-gray elevation ramp for reports/paper export
  (`plot_bg` is pure white).

Two built-in **styles** (visual languages):

- **modern** (default) — the clean flat MEDUSA look (`assets/qss/base.qss`).
- **presentation** — larger type, thicker plot lines and roomier controls for
  projectors and slides (token-only; reuses `base.qss`).

The data-visualization encodings — a 16-color colorblind-aware categorical cycle
and the `medusa_sequential` (PSD) / `medusa_diverging` (topomap) colormaps — are
**theme- and style-independent**, so results stay comparable across every look.

## Customize

Zero config by default; override in one line. `colors` derive a new **theme**,
`style` derives a new **style**:

```python
ms.customize(
    colors={"accent_primary": "#FF6600"},                # -> a registered theme
    style={"ui_size_body": 15, "plot_line_width": 1.4},  # -> a registered style
)                            # activates + live-restyles; returns the created value(s)
```

## Architecture & extensibility

```
medusa_style/
  api.py        the public verbs
  dispatch.py   target detection + environment routing (stdlib-only)
  appearance.py the active (theme, style) + live switching + observer
  palette.py    the color SSOT (Palette, DARK, LIGHT) + encodings
  style.py      the visual languages (Style, MODERN, PRESENTATION)
  resources.py  zip-safe asset loading
  qt/           the Qt environment (QSS render, PlotPanel, icons, fonts)
  mpl/          the Matplotlib environment (rcParams, colormaps, figure styling)
  assets/       qss/ (structural templates), icons/, fonts/, brand/
```

- **Add a theme** — one `Palette(...)` literal in `palette.py` (+ `PALETTES`
  entry) or `ms.register_theme(...)`. Works in every environment immediately.
- **Add a style** — one `Style(...)` literal in `style.py` (+ `STYLES` entry) or
  `ms.register_style(...)`. A token-only style is pure data; only a structurally
  different widget language needs its own `assets/qss/<name>.qss`.
- **Add an environment** (Plotly, VisPy, Bokeh, …) — a new package with a
  `matches`/`_apply`/`reapply` trio and one `ms.register_environment(...)`. No
  existing file changes.

## Power-user namespaces

The tiny top-level surface is enough for almost everything; reach into these only
when you need the knobs:

| Namespace              | What's there |
|------------------------|--------------|
| `medusa_style.qt`      | `application()`, `PlotPanel`, `embed_figure()`, `style_app()`, `stylesheet()`, `prepare_high_dpi()`, `icon()`, `pixmap()`, `load_fonts()` |
| `medusa_style.mpl`     | `style_figure()`, `style_axes()`, `rcparams()`, `build_rcparams()`, `mplstyle_text()`, `register_colormaps()`, `sequential_cmap()`, `diverging_cmap()` |
| `medusa_style.palette` | the raw SSOT colors: `Palette`, `DARK`, `LIGHT`, `CATEGORICAL_CYCLE`, WCAG utils |
| `medusa_style.style`   | the visual languages: `Style`, `MODERN`, `PRESENTATION` |

`medusa_style.qt` imports PySide6 (never Matplotlib); `medusa_style.mpl` imports
Matplotlib (never Qt). `import medusa_style` loads neither until you style
something, so a headless figure-export job never pulls in Qt.

## Icons, fonts & brand assets

```python
from medusa_style import qt

button.setIcon(qt.icon("save_as"))        # theme-agnostic action icon
button.setIcon(qt.icon("delete_forever", color="error"))  # ...or a semantic color
splash = qt.pixmap("medusa_splash")       # brand images (splash/login/about)
win.setWindowIcon(qt.app_icon())          # the MEDUSA app icon
path = qt.asset_path("fonts/Roboto-Regular.ttf")   # any bundled file, real path
```

**`qt.icon(name)` icons are theme-agnostic.** The color is not baked in: each
icon recolors to the active theme on every repaint and changes with Qt's state
automatically — a disabled widget dims its icon, and a theme switch repaints it
live, with no re-fetch. Pass `color=` (a palette role like `"error"`/`"accent_primary"`,
or a `"#RRGGBB"` literal) to force a specific color; it still dims when disabled.

`qt.application()` does the common setup for you: it registers the bundled fonts
(Roboto family, Abel, Dense, Womby) and sets the MEDUSA window icon app-wide
(both opt-out via `application(set_icon=False, load_bundled_fonts=False)`).

## Packaging

All runtime data lives under `medusa_style/assets/` — `qss/` (the structural QSS
templates), `icons/`, `fonts/` and `brand/`. They are declared in `pyproject.toml`
so the wheel always includes them, while the editable `.psd` design sources stay
in the repo-level `design_source/` directory (outside the package, never shipped):

```toml
[tool.hatch.build.targets.wheel]
packages  = ["medusa_style"]
artifacts = [
    "medusa_style/assets/**/*.qss",
    "medusa_style/assets/**/*.svg",
    "medusa_style/assets/**/*.png",
    "medusa_style/assets/**/*.ico",
    "medusa_style/assets/**/*.ttf",
    "medusa_style/assets/**/*.otf",
    "medusa_style/py.typed",
]
```

At runtime the assets are loaded zip-safely via `medusa_style.resources` (which
wraps `importlib.resources`), so it works even from inside a zipped wheel.
Matplotlib `.mplstyle` files are generated on demand
(`medusa_style.mpl.write_mplstyle(...)`) rather than shipped, so they can never
drift from the SSOT.
```
```
