Flash Animation System
======================

**Game engine-style O(1) per-window flash animations for UI feedback.**

*Module: pyqt_reactive.animation.flash_mixin*

Overview
--------

The flash animation system provides visual feedback when configuration values change.
It uses a game engine architecture to achieve O(1) rendering per window regardless
of how many elements are flashing.

Architecture
------------

The system consists of three core components:

1. **_GlobalFlashCoordinator** (singleton): ONE 60fps timer for ALL windows
2. **WindowFlashOverlay** (per-window): Renders ALL flash rectangles in ONE paintEvent
3. **FlashMixin** (per-widget): API for registering elements and triggering flashes

.. code-block:: text

   ┌─────────────────────────────────────────────────────────────┐
   │                  _GlobalFlashCoordinator                    │
   │  ┌─────────────────┐  ┌──────────────────────────────────┐  │
   │  │ _flash_start_   │  │ _computed_colors: Dict[key, QColor] │
   │  │   times: Dict   │  │ (pre-computed each tick)          │  │
   │  └─────────────────┘  └──────────────────────────────────┘  │
   │                              │                              │
   │                              ▼                              │
   │                    [60fps timer tick]                       │
   │                              │                              │
   └──────────────────────────────┼──────────────────────────────┘
                                  │
           ┌──────────────────────┼──────────────────────┐
           ▼                      ▼                      ▼
   ┌───────────────┐      ┌───────────────┐      ┌───────────────┐
   │WindowFlashOverlay│   │WindowFlashOverlay│   │WindowFlashOverlay│
   │   (Window A)  │      │   (Window B)  │      │   (Window C)  │
   │               │      │               │      │               │
   │ ONE paintEvent│      │ ONE paintEvent│      │ ONE paintEvent│
   │ renders ALL   │      │ renders ALL   │      │ renders ALL   │
   │ flash rects   │      │ flash rects   │      │ flash rects   │
   └───────────────┘      └───────────────┘      └───────────────┘

Performance Model
-----------------

**Before (O(n) per tick):**

.. code-block:: text

   Timer tick → compute N colors → store in dict → N widget repaints

**After (O(1) per window):**

.. code-block:: text

   Timer tick → compute colors once → prune expired → ONE overlay.update() per window

Each ``WindowFlashOverlay.paintEvent()`` renders all flash rectangles for its window
in a single paint call. Geometry is cached and only recomputed on scroll/resize.

Animation Phases
----------------

Flash animations have three phases with configurable durations:

1. **fade_in** (100ms): Quick fade-in with OutQuad easing
2. **hold** (50ms): Hold at maximum intensity
3. **fade_out** (350ms): Slow fade-out with InOutCubic easing

Widget-Type-Specific Masking
--------------------------------

Flash animations use widget-type-specific masking strategies for precise visual feedback:

**Masking Strategies**:

- **Checkbox**: Tight mask for indicator + label text using Qt style subelement rects
- **Label**: Tight mask using ``sizeHint()`` to avoid empty layout space
- **Help Button**: Fixed square mask when ``_square_size`` is set
- **All other widgets**: Full rectangle mask

**Checkbox Square Cutout**:

Textless checkboxes (no label) use square cutouts to avoid rounding:

.. code-block:: python

    def _needs_square_checkbox_mask(widget: QWidget) -> bool:
        return isinstance(widget, QCheckBox) and not widget.text()

**Function Pane Title Masking**:

Function panes mask title row widgets tightly:

.. code-block:: python

    def _get_function_pane_title_widgets(groupbox: QWidget) -> List[QWidget]:
        pane = groupbox
        while pane is not None:
            if hasattr(pane, "_flash_title_container") or hasattr(pane, "_module_path_label"):
                break
            pane = pane.parentWidget()

        widgets = []
        module_label = getattr(pane, "_module_path_label", None)
        if module_label and module_label.isVisible():
            widgets.append(module_label)

        title_container = getattr(pane, "_flash_title_container", None)
        if title_container and title_container.isVisible():
            for child in title_container.findChildren(QWidget):
                if child.isVisible() and isinstance(child, LEAF_WIDGET_TYPES):
                    widgets.append(child)

        return widgets

FlashElement Types
------------------

The system supports multiple element types via ``FlashElement`` dataclass:

.. list-table::
   :header-rows: 1

   * - Element Type
     - Factory Function
     - Use Case
   * - Groupbox
     - ``create_groupbox_element()``
     - Form section headers (STANDARD mode masks all children, INVERSE mode masks title + leaf_widget)
   * - Groupbox (full rect)
     - ``create_groupbox_element(..., use_full_rect=True)``
     - Flash entire groupbox geometry (no margin-top offset)
   * - Tree Item
     - ``create_tree_item_element()``
     - Config hierarchy trees
   * - List Item
     - ``create_list_item_element()``
     - Step/function lists

INVERSE Mode with Label Widget Masking
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

INVERSE mode now masks title + leaf_widget + label_widget (not all title row widgets):

.. code-block:: python

    self.register_flash_leaf(
        key="my_field",
        groupbox=my_groupbox,
        leaf_widget=my_widget,
        label_widget=my_label  # NEW: mask label too
    )

This highlights "all fields that inherited the change" while keeping the changed field and its label visible.

**Masking Behavior**:

- **STANDARD mode** (``leaf_widget=None``): Mask ALL children, flash only frame/background
- **INVERSE mode** (``leaf_widget=widget``): Mask title + leaf_widget + label_widget, flash frame + all siblings

Usage with FlashMixin
---------------------

Widgets inherit ``FlashMixin`` (alias: ``VisualUpdateMixin``) to participate:

.. code-block:: python

   from pyqt_reactive.animation.flash_mixin import FlashMixin

   class MyWidget(QWidget, FlashMixin):
       def __init__(self):
           super().__init__()
           self._init_flash_mixin()

       def setup_flash(self, groupbox: QGroupBox):
           # Register element for flashing
           self.register_flash_groupbox("my_key", groupbox)

       def trigger_flash(self):
           # Trigger flash (global - all windows with this key)
           self.queue_flash("my_key")

           # Or local flash (this window only)
           self.queue_flash_local("my_key")

Scope-Based Flash Keys
----------------------

Flash keys are automatically scoped to prevent cross-window contamination:

.. code-block:: python

   # Key "well_filter" becomes "orchestrator::plate_1::well_filter"
   scoped_key = self._get_scoped_flash_key("well_filter")

This ensures flashing ``step_0`` in plate1 window doesn't flash ``step_0`` in plate2.

OpenGL Acceleration
-------------------

On systems with OpenGL 3.3+, the system uses ``WindowFlashOverlayGL`` for GPU-accelerated
rendering via instanced draw calls. Falls back to QPainter automatically.

See Also
--------

- :doc:`flash_callback_system` - How ObjectState triggers flash callbacks
- :doc:`gui_performance_patterns` - Cross-window preview system
- :doc:`abstract_manager_widget` - AbstractManagerWidget uses FlashMixin

