List Item Preview System
========================

The list item preview system provides declarative, per-field styling for list items in manager widgets (PlateManager, PipelineEditor). It displays configuration previews with visual indicators for dirty state and signature differences.

.. contents:: Table of Contents
   :local:
   :depth: 2

Overview
--------

List items in pyqt-reactive show rich previews of configuration state:

.. code-block:: text

   ▶ MyPlate (W:8 | Seq:C,Z)           ← Inline format
     /path/to/plate
     └─ wf:[3] | mat:ZARR | configs=[NAP, FIJI]

**Visual semantics:**

- **Underline** = field differs from signature default (explicitly set)
- **Asterisk (*)** = resolved value differs from saved (dirty/unsaved)
- Name inherits styling if ANY field is dirty/sig-diff

Declarative Configuration
-------------------------

Each widget declares its format using ``ListItemFormat``:

.. code-block:: python

   from pyqt_reactive.widgets.shared.abstract_manager_widget import ListItemFormat

   class PlateManagerWidget(AbstractManagerWidget):
       LIST_ITEM_FORMAT = ListItemFormat(
           first_line=(),                    # Fields after name on line 1
           preview_line=(                    # Fields on └─ preview line
               'num_workers',
               'vfs_config.materialization_backend',
               'path_planning_config.well_filter',
           ),
           detail_line_field='path',         # Field for detail line
           show_config_indicators=True,      # Show NAP/FIJI/MAT configs
           formatters={                      # Custom formatters (optional)
               'my_field': lambda v: f"custom:{v}" if v else None,
           },
       )

Field abbreviations are declared on config classes via ``@global_pipeline_config``:

.. code-block:: python

   @global_pipeline_config(field_abbreviations={'well_filter': 'wf', 'output_dir_suffix': 'out'})
   @dataclass(frozen=True)
   class PathPlanningConfig:
       well_filter: Optional[str] = None
       output_dir_suffix: str = "_pyqt_reactive"

ListItemFormat Fields
~~~~~~~~~~~~~~~~~~~~~

.. list-table::
   :header-rows: 1
   :widths: 25 15 60

   * - Field
     - Default
     - Description
   * - ``first_line``
     - ``()``
     - Tuple of field paths shown after name on line 1
   * - ``preview_line``
     - ``()``
     - Tuple of field paths shown on └─ preview line
   * - ``detail_line_field``
     - ``None``
     - Single field path for detail line (e.g., ``'path'``)
   * - ``show_config_indicators``
     - ``True``
     - Whether to show config indicators (NAP, FIJI, MAT)
   * - ``formatters``
     - ``{}``
     - Dict mapping field path to formatter function

Visual Layout
-------------

Multiline Format
~~~~~~~~~~~~~~~~

.. code-block:: text

   ▶ ItemName (first_line_segments)     ← Line 1: name + first_line fields
     detail_line                         ← Line 2: e.g., path
     └─ preview_segments | configs=[NAP, FIJI]  ← Line 3: preview + configs

Inline Format
~~~~~~~~~~~~~

.. code-block:: text

   ▶ ItemName  (seg1 | seg2 | seg3)     ← Single line with all segments

The format is determined by the ``StyledTextLayout.multiline`` flag, set automatically based on whether detail_line or preview_line are populated.

Field Types
-----------

Permanent vs Dynamic Fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. list-table::
   :header-rows: 1
   :widths: 20 30 50

   * - Type
     - Source
     - Behavior
   * - **Declared**
     - ``first_line``, ``preview_line``
     - Always shown (permanent)
   * - **Config indicators**
     - ``@global_pipeline_config(preview_label='NAP')``
     - Shown if config's ``enabled=True``
   * - **Sig-diff fields**
     - Auto-detected
     - Dynamically added when value differs from signature default

The auto-include logic (in ``_build_multiline_styled_text``) automatically adds any field with a non-default value to the preview line, even if not explicitly declared.

Value Formatting
----------------

All field values are formatted by ``format_preview_value()`` in ``config_preview_formatters.py``:

.. code-block:: python

   def format_preview_value(value: Any) -> Optional[str]:
       if value is None:
           return None
       if isinstance(value, Enum):
           return value.name if value.value else None
       if isinstance(value, list):
           if value and isinstance(value[0], Enum):
               return ','.join(v.value for v in value)
           return f'[{len(value)}]'
       if callable(value) and not isinstance(value, type):
           return getattr(value, '__name__', str(value))
       return str(value)

Field abbreviations are declared on config classes via ``@global_pipeline_config(field_abbreviations=...)``:

.. code-block:: python

   @global_pipeline_config(field_abbreviations={'well_filter': 'wf', 'output_dir_suffix': 'out'})
   @dataclass(frozen=True)
   class PathPlanningConfig:
       well_filter: Optional[str] = None
       output_dir_suffix: str = "_pyqt_reactive"

Adding a New Preview Field
--------------------------

1. **Add to ListItemFormat** (in widget):

.. code-block:: python

   LIST_ITEM_FORMAT = ListItemFormat(
       preview_line=(
           'existing_field',
           'my_new_config.some_field',  # Add field path
       ),
       formatters={
           'some_field': lambda v: f"custom:{v}" if v else None,  # Optional formatter
       },
   )

2. **Add abbreviation** (on config class):

.. code-block:: python

   @global_pipeline_config(field_abbreviations={'some_field': 'sf'})
   @dataclass(frozen=True)
   class MyNewConfig:
       some_field: str = "default"

Adding a Config Indicator
-------------------------

Config indicators (NAP, FIJI, MAT) and field abbreviations are both declared via ``@global_pipeline_config``:

.. code-block:: python

   from objectstate import global_pipeline_config

   @global_pipeline_config(
       preview_label='NEW',
       field_abbreviations={'some_setting': 'set'}
   )
   @dataclass
   class MyNewConfig:
       enabled: bool = False
       some_setting: int = 10

The config indicator appears in ``configs=[...]`` when ``enabled=True``.

Structured Rendering Architecture
---------------------------------

The system uses structured ``StyledTextLayout`` objects instead of string parsing:

.. code-block:: python

   @dataclass(frozen=True)
   class Segment:
       """A styled text segment with field path for dirty/sig-diff matching."""
       text: str
       field_path: Optional[str] = None  # None=no styling, ''=root path

   @dataclass
   class StyledTextLayout:
       """Structured layout for styled text rendering."""
       name: Segment
       status_prefix: str = ""
       first_line_segments: List[Segment] = field(default_factory=list)
       detail_line: str = ""
       preview_segments: List[Segment] = field(default_factory=list)
       config_segments: List[Segment] = field(default_factory=list)
       multiline: bool = False

The delegate iterates segments directly and applies styling per-segment without any string matching:

1. Build ``StyledTextLayout`` with all segments
2. Store layout in item data (``LAYOUT_ROLE``)
3. Delegate reads layout and renders segments with appropriate styling
4. No string parsing required

See Also
--------

- :doc:`abstract_manager_widget` - AbstractManagerWidget base class
- :doc:`gui_performance_patterns` - Cross-window preview system
- :doc:`configuration_framework` - Config framework and lazy configs

**Implementation References:**

- ``pyqt_reactive/widgets/shared/list_item_delegate.py`` - StyledTextLayout and rendering
- ``pyqt_reactive/widgets/shared/abstract_manager_widget.py`` - ListItemFormat and build methods
- ``pyqt_reactive/utils/preview_formatters.py`` - Centralized formatters
