Metadata-Version: 2.4
Name: inspyre-splash
Version: 0.2.0
Summary: A small PySide6 splash screen composer with layered images and animated typography.
Project-URL: Homepage, https://github.com/Inspyre-Softworks/inspyre-splash
Author: Taylor B. | Inspyre-Softworks
License-Expression: MIT
License-File: LICENSE
Keywords: animation,desktop,pyside6,splash-screen
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: User Interfaces
Requires-Python: <3.15,>=3.11
Requires-Dist: pillow>=10
Requires-Dist: platformdirs>=4
Requires-Dist: pyside6>=6.6
Description-Content-Type: text/markdown

# InspyreSplash

[![Documentation Status](https://readthedocs.org/projects/inspyre-splash/badge/?version=latest)](https://inspyre-splash.readthedocs.io/en/latest/?badge=latest)
[![PyPI version](https://img.shields.io/pypi/v/inspyre-splash.svg)](https://pypi.org/project/inspyre-splash/)
[![Python versions](https://img.shields.io/pypi/pyversions/inspyre-splash.svg)](https://pypi.org/project/inspyre-splash/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

`inspyre-splash` is a small PySide6-powered splash screen composer for Python desktop apps. It builds transparent, frameless splash windows from ordered layers: static images, animated images decoded by Pillow, and animated typography.

Author: Taylor B. | Inspyre-Softworks

This is an MVP. It intentionally avoids progress bars, themes, audio, video, Lottie, and other extras so the public API stays practical.

## Features

- Transparent, frameless PySide6 splash windows.
- Ordered layers where earlier layers render behind later layers.
- Static PNG/JPEG/WebP image layers backed by `QPixmap`.
- Animated WebP, GIF, and APNG layers decoded through Pillow.
- Text sequence layers with built-in `FadeIn`, `FlyIn`, `Typewriter`, `WipeIn`, and `ExplodeIn` effects.
- Optional text backgrounds, opacity, shadows, custom fonts, absolute positioning, and loop control.
- Blocking `run_until()` startup work for app launch flows.
- Non-blocking `start_until()` startup work for IPython, ptipython, notebooks, and other interactive prompts.
- Cooperative cancellation through `SplashTask.cancel()` and caller-owned `threading.Event` objects.
- Headless-friendly tests with `QT_QPA_PLATFORM=offscreen`.

## Documentation

The full Sphinx documentation is configured for Read the Docs and lives under `docs/`.

```powershell
poetry install --with docs --no-interaction
poetry run sphinx-build -W -b html docs docs/_build/local-html
```

Read the Docs builds from `docs/conf.py` using `.readthedocs.yaml`.

## Install

From the project root:

```powershell
poetry install
```

For an editable pip install:

```powershell
python -m pip install -e .
```

## Basic Example

```python
from pathlib import Path

from inspyre_splash import Splash
from inspyre_splash.layers import AnimatedImageLayer

splash = Splash(width=700, height=700, transparent=True, stay_on_top=True)
splash.add_layer(
    AnimatedImageLayer(
        Path('assets/transparent_splash.webp'),
        loops=None,
        scale=1.0,
        position='center',
    )
)
splash.close_after_ms(5000)
splash.show()
splash.run()
```

The splash can also be used as a context manager. Exiting the block stops all layers and closes the splash widget:

```python
with Splash(width=700, height=700) as splash:
    splash.add_layer(AnimatedImageLayer(Path('assets/transparent_splash.webp')))
    splash.close_after_ms(5000)
    splash.show()
    splash.run()
```

For startup work, use `run_until()`. It runs your callable while the splash is active, keeps Qt's event loop alive for animations, then closes the splash when the callable returns:

```python
def load_application():
    return build_main_window()


with Splash(width=700, height=700) as splash:
    splash.add_layer(AnimatedImageLayer(Path('assets/transparent_splash.webp')))
    main_window = splash.run_until(load_application)
    splash.finish(main_window)
    splash.run()
```

For an interactive prompt, notebook, or `ptipython` session where you want the prompt back immediately, use `start_until()` instead. It shows the splash, starts your callable on a worker thread, and returns a `SplashTask` handle.

```python
%gui qt

splash = Splash(width=1200, height=1200, transparent=True, stay_on_top=True)
splash.add_layer(ImageLayer(Path(PNG), scale=1.0, position='center'))
splash.add_layer(AnimatedImageLayer(Path(WEBP), scale=1.0, position='center'))
splash.add_layer(text_sequence)

task = splash.start_until(__load__, 5, cancel_kwarg='cancel_event')
```

`start_until()` does not use the `Splash` context manager because leaving a `with Splash(...)` block closes the splash. Keep `splash` and `task` assigned until the work is done. It also shows the splash by default, so do not call `splash.show()` or `splash.run()` after `start_until()`.

To cancel from the prompt, call:

```python
task.cancel()
```

Cancellation is cooperative: the worker callable must accept and check the injected `cancel_event` to stop its own Python work:

```python
def __load__(temporal_pad: int, cancel_event):
    for _ in range(temporal_pad):
        if cancel_event.is_set():
            return 'cancelled'
        sleep(1)
    return 'loaded'
```

Later, inspect the result or any exception:

```python
task.done()
task.result()
task.exception()
```

If you prefer to own the event yourself, pass it to your worker and then either set it directly or call `task.cancel()`:

```python
event = Event()
task = splash.start_until(__load__, 5, event)

event.set()
# or:
task.cancel()
```

`splash.run()` blocks while Qt's event loop is active. If you manage your own callbacks, use `splash.stop()` or `splash.exit()` when the work is done and you want `run()` to return. `stop()` is safe to call from a worker thread because it queues the shutdown back onto Qt's thread.

For synchronous code inside a `with Splash(...)` block, use `splash.leave()` to close the splash and leave the context immediately:

```python
with Splash(width=700, height=700) as splash:
    splash.show()

    if startup_failed:
        splash.leave()

    splash.run()
```

Use `leave()` from normal Python control flow. Use `exit()` from Qt callbacks, because exceptions raised inside Qt callbacks do not reliably unwind the Python context manager.

## Package Asset Discovery

Host applications can keep splash assets beside their own package and let `inspyre-splash` discover them. Discovery is on by default for the helper calls, but it can be disabled per call, globally, or with the `INSPYRE_SPLASH_AUTO_DISCOVERY=0` environment variable.

Supported package-side layouts:

```text
your_app/
  splash.json
  splash/
    intro/
      splash.json
      transparent_splash.webp
      glow.png
    updater/
      splash.json
      updater.webp
  assets/
    splashes/
      release/
        inspyre-splash.json
        release.webp
```

The same layouts are supported below `PlatformDirs(appname='your_app').user_data_path`, which lets users or installers override bundled splash assets without writing inside the package directory.

From inside the host package, call `auto_splash()` without arguments and it will infer the calling package:

```python
from inspyre_splash import auto_splash


def launch():
    splash = auto_splash(name='intro')
    if splash is None:
        return load_application()

    main_window = splash.run_until(load_application)
    splash.finish(main_window)
    splash.run()
```

You can also be explicit, which is nicer for tests and command-line entry points:

```python
from inspyre_splash import auto_splash, configure_auto_discovery

configure_auto_discovery(True)
splash = auto_splash('your_app', enabled=True)
```

A splash JSON definition can describe window options and ordered layers:

```json
{
  "name": "intro",
  "splash": {
    "width": 700,
    "height": 700,
    "transparent": true,
    "stay_on_top": true
  },
  "layers": [
    {
      "type": "image",
      "path": "glow.png",
      "scale": 1.15,
      "position": "center"
    },
    {
      "type": "animated_image",
      "path": "transparent_splash.webp",
      "loops": null,
      "scale": 1.0,
      "position": "center"
    },
    {
      "type": "text_sequence",
      "font_family": "Segoe UI",
      "font_size": 34,
      "color": "#ffffff",
      "x": 70,
      "y": 565,
      "background_color": "#111827",
      "background_opacity": 0.62,
      "shadow_color": "#000000",
      "shadow_opacity": 0.7,
      "loops": null,
      "entries": [
        {"text": "TAYLOR SUITE", "effect": "wipe_in"},
        {"text": "Loading modules...", "effect": {"type": "fly_in", "direction": "bottom"}},
        {"text": "Almost there...", "effect": {"type": "typewriter", "speed_ms": 42}},
        {"text": "READY", "effect": "explode_in"}
      ]
    }
  ]
}
```

Layer paths and custom font paths are resolved relative to the folder containing the JSON file, so every animation can be a self-contained folder.

## Layered Typography Example

```python
from pathlib import Path

from inspyre_splash import Splash
from inspyre_splash.effects import ExplodeIn, FlyIn, Typewriter, WipeIn
from inspyre_splash.layers import AnimatedImageLayer, ImageLayer, TextSequenceLayer

splash = Splash(width=700, height=700, transparent=True, stay_on_top=True)

splash.add_layer(
    ImageLayer(
        Path('assets/transparent_45deg_glow_ellipse.png'),
        scale=1.15,
        position='center',
    )
)
splash.add_layer(
    AnimatedImageLayer(
        Path('assets/transparent_splash.webp'),
        loops=None,
        scale=1.0,
        position='center',
    )
)
splash.add_layer(
    TextSequenceLayer(
        entries=[
            ('TAYLOR SUITE', WipeIn()),
            ('Loading modules...', FlyIn(direction='bottom')),
            ('Almost there...', Typewriter(speed_ms=42)),
            ('READY', ExplodeIn()),
        ],
        font_family='Segoe UI',
        font_size=34,
        color='#ffffff',
        x=70,
        y=565,
        background_color='#111827',
        background_opacity=0.62,
        shadow_color='#000000',
        shadow_opacity=0.7,
        shadow_x_offset=3,
        shadow_y_offset=3,
        loops=None,
    )
)

splash.show()
splash.run()
```

## Development

Use Poetry for contributor and AI-helper workflows:

```powershell
poetry install --with dev --no-interaction
$env:QT_QPA_PLATFORM = 'offscreen'
poetry run python -m unittest discover -s tests
poetry run python -m compileall src tests examples docs
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contributor workflow.

## Notes

Animated WebP, GIF, and APNG files are decoded through Pillow with `ImageSequence`. Per-frame durations are respected when Pillow exposes them.

Layer order is simple: earlier layers are behind later layers. Add a static transparent PNG first, then an animated WebP, then text if the text should render above the images.

Text sequence layers use `position='center'` by default, or `position='bottom'` for bottom placement. Pass `x` and `y` to place the text at an exact top-left pixel inside the transparent splash window. Pass `background_color` and `background_opacity` to paint a colored backing behind the text. Pass `shadow_color`, `shadow_opacity`, `shadow_x_offset`, and `shadow_y_offset` to paint a drop shadow with the text.

Image layers fit inside the splash bounds by default so oversized PNG/WebP/GIF/APNG assets are not unexpectedly clipped. Pass `fit_to_parent=False` if you want native pixel sizing where an oversized layer can intentionally bleed outside the splash.

Animated image layers and text sequence layers accept `loops=None` for forever or an integer for a finite number of loops. Any layer can be interrupted with `layer.interrupt()`; animated layers stop their timers, text layers stop the active effect, and static layers hide themselves.

Transparent window behavior can vary by operating system, display server, and window compositor. The package requests a translucent, frameless PySide6 window, but final blending is controlled by the host OS.
