"""API description for managing plotting theme parameters in pyvista.

Examples
--------
Apply a built-in theme

>>> import pyvista as pv
>>> pv.set_plot_theme('default')
>>> pv.set_plot_theme('document')
>>> pv.set_plot_theme('dark')
>>> pv.set_plot_theme('paraview')

Load a theme into pyvista

>>> from pyvista.plotting.themes import DocumentTheme
>>> theme = DocumentTheme()
>>> theme.save('my_theme.json')  # doctest:+SKIP
>>> loaded_theme = pv.load_theme('my_theme.json')  # doctest:+SKIP

Create a custom theme from the default theme and load it into
pyvista.

>>> my_theme = DocumentTheme()
>>> my_theme.font.size = 20
>>> my_theme.font.title_size = 40
>>> my_theme.cmap = 'jet'
>>> pv.global_theme.load_theme(my_theme)
>>> pv.global_theme.font.size
20

"""

from __future__ import annotations

from enum import Enum
import json
import os
import pathlib
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from typing import ClassVar

import pyvista  # noqa: TC001
from pyvista._warn_external import warn_external
from pyvista.core.config import _ConfigBase
from pyvista.core.utilities.misc import _check_range

from .colors import Color
from .colors import get_cmap_safe
from .colors import get_cycler
from .interactor_style_registry import _validate_interactor_style
from .opts import InterpolationType
from .opts import PointSpriteShape
from .theme_registry import _available_theme_names
from .theme_registry import _register_alias
from .theme_registry import _register_theme_class
from .theme_registry import _resolve_dotted_path
from .theme_registry import _resolve_theme
from .tools import parse_font_family

if TYPE_CHECKING:
    from collections.abc import Callable

    from pyvista.core._typing_core import VectorLike

    from ._typing import ColorLike
    from ._typing import ColormapOptions


def _set_plot_theme_from_env() -> None:
    """Set plot theme from an environment variable."""
    if 'PYVISTA_PLOT_THEME' in os.environ:
        theme = os.environ['PYVISTA_PLOT_THEME']
        try:
            # Dotted paths are case-sensitive; registered names are lowercased.
            set_plot_theme(theme if ':' in theme else theme.lower())
        except ValueError:
            allowed = ', '.join(_available_theme_names())
            warn_external(
                f'\n\nInvalid PYVISTA_PLOT_THEME environment variable "{theme}". '
                f'Should be one of {{ {allowed} }} or "package.module:ClassName".',
            )


def load_theme(filename):
    """Load a theme from a file.

    Parameters
    ----------
    filename : str
        Theme file. Must be json.

    Returns
    -------
    pyvista.Theme
        The loaded theme.

    Examples
    --------
    >>> import pyvista as pv
    >>> from pyvista.plotting.themes import DocumentTheme
    >>> theme = DocumentTheme()
    >>> theme.save('my_theme.json')  # doctest:+SKIP
    >>> loaded_theme = pv.load_theme('my_theme.json')  # doctest:+SKIP

    """
    with Path(filename).open() as f:
        theme_dict = json.load(f)
    return Theme.from_dict(theme_dict)


def set_plot_theme(theme):
    """Set plotting parameters to a predefined theme using a string or ``Theme``.

    Parameters
    ----------
    theme : str | Theme
        Theme to apply. Accepts any of:

        * A registered theme name. Built-in names include ``'dark'``,
          ``'default'``, ``'document'``, ``'document_build'``,
          ``'document_pro'``, ``'paraview'``, ``'testing'``, and
          ``'vtk'``. Third-party plugins can add more via the
          ``pyvista.themes`` entry-point group. Use
          :func:`~pyvista.registered_themes` to list everything that is
          currently available.
        * A ``"package.module:ClassName"`` dotted path to any importable
          :class:`~pyvista.plotting.themes.Theme` subclass.
        * A :class:`~pyvista.plotting.themes.Theme` instance.

    See Also
    --------
    pyvista.registered_themes
        List all registered theme names.
    pyvista.plotting.themes.Theme
        Base class. Subclasses with a class-level ``_default_name`` are
        discoverable by name; see the class docstring for details aimed
        at theme authors and plugin packages.

    Examples
    --------
    Set to the default theme.

    >>> import pyvista as pv
    >>> pv.set_plot_theme('default')

    Set to the document theme.

    >>> pv.set_plot_theme('document')

    Set to the dark theme.

    >>> pv.set_plot_theme('dark')

    Set to the ParaView theme.

    >>> pv.set_plot_theme('paraview')

    Load a theme from any importable module using a dotted path.

    >>> pv.set_plot_theme('pyvista.plotting.themes:DarkTheme')

    """
    import pyvista  # noqa: PLC0415

    if isinstance(theme, str):
        if ':' in theme:
            cls = _resolve_dotted_path(theme)
            pyvista.global_theme.load_theme(cls())
            return
        resolved = _resolve_theme(theme)
        if resolved is None:
            allowed = ', '.join(_available_theme_names())
            msg = (
                f'Theme "{theme}" not found. Available themes: {allowed}. '
                'To load from an arbitrary module use "package.module:ClassName".'
            )
            raise ValueError(msg)
        pyvista.global_theme.load_theme(resolved)
    elif isinstance(theme, Theme):
        pyvista.global_theme.load_theme(theme)
    else:
        msg = (
            f'Expected a ``pyvista.plotting.themes.Theme`` or ``str``, not {type(theme).__name__}'
        )
        raise TypeError(msg)


class _LightingConfig(_ConfigBase):
    """PyVista lighting configuration.

    This will control the lighting interpolation type, parameters,
    and Physically Based Rendering (PBR) options

    Examples
    --------
    Set global PBR parameters.

    >>> import pyvista as pv
    >>> pv.global_theme.lighting_params.interpolation = 'pbr'
    >>> pv.global_theme.lighting_params.metallic = 0.5
    >>> pv.global_theme.lighting_params.roughness = 0.25

    """

    __slots__ = [
        '_ambient',
        '_diffuse',
        '_emissive',
        '_interpolation',
        '_metallic',
        '_roughness',
        '_specular',
        '_specular_power',
    ]

    def __init__(self):
        self._interpolation = InterpolationType.FLAT.value
        self._metallic = 0.0
        self._roughness = 0.5
        self._ambient = 0.0
        self._diffuse = 1.0
        self._specular = 0.0
        self._specular_power = 100.0
        self._emissive = False

    @property
    def interpolation(self) -> InterpolationType:  # numpydoc ignore=RT01
        """Return or set the default interpolation type.

        See :class:`pyvista.plotting.opts.InterpolationType`.

        Options are:

        * ``'Phong'``
        * ``'Flat'``
        * ``'Physically based rendering'``

        This is stored as an integer value of the ``InterpolationType``
        so that the theme can be JSON-serializable.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.interpolation = 'Phong'
        >>> pv.global_theme.lighting_params.interpolation
        <InterpolationType.PHONG: 2>


        """
        return InterpolationType.from_any(self._interpolation)

    @interpolation.setter
    def interpolation(
        self,
        interpolation: str | int | InterpolationType,
    ):
        self._interpolation = InterpolationType.from_any(interpolation).value

    @property
    def metallic(self) -> float:  # numpydoc ignore=RT01
        """Return or set the metallic value.

        This requires that the interpolation be set to ``'Physically based
        rendering'``. Must be between 0 and 1.

        Examples
        --------
        Set the global metallic value used in physically based rendering to
        ``0.5``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.metallic = 0.5
        >>> pv.global_theme.lighting_params.metallic
        0.5

        """
        return self._metallic

    @metallic.setter
    def metallic(self, metallic: float):
        _check_range(metallic, (0, 1), 'metallic')
        self._metallic = metallic

    @property
    def roughness(self) -> float:  # numpydoc ignore=RT01
        """Return or set the roughness value.

        This value has to be between 0 (glossy) and 1 (rough). A glossy
        material has reflections and a high specular part. This parameter is
        only used by PBR interpolation.

        Examples
        --------
        Set the global roughness value used in physically based rendering to
        ``0.25``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.roughness = 0.25
        >>> pv.global_theme.lighting_params.roughness
        0.25

        """
        return self._roughness

    @roughness.setter
    def roughness(self, roughness: float):
        _check_range(roughness, (0, 1), 'roughness')
        self._roughness = roughness

    @property
    def ambient(self) -> float:  # numpydoc ignore=RT01
        """Return or set the ambient value.

        When lighting is enabled, this is the amount of light in the range of 0
        to 1 that reaches the actor when not directed at the light source
        emitted from the viewer.

        Examples
        --------
        Set the global ambient lighting value to ``0.2``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.ambient = 0.2
        >>> pv.global_theme.lighting_params.ambient
        0.2

        """
        return self._ambient

    @ambient.setter
    def ambient(self, ambient: float):
        _check_range(ambient, (0, 1), 'ambient')
        self._ambient = ambient

    @property
    def diffuse(self) -> float:  # numpydoc ignore=RT01
        """Return or set the diffuse value.

        This is the scattering of light by reflection or
        transmission. Diffuse reflection results when light strikes an
        irregular surface such as a frosted window or the surface of a
        frosted or coated light bulb. Must be between 0 and 1.

        Examples
        --------
        Set the global diffuse lighting value to ``0.5``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.diffuse = 0.5
        >>> pv.global_theme.lighting_params.diffuse
        0.5

        """
        return self._diffuse

    @diffuse.setter
    def diffuse(self, diffuse: float):
        _check_range(diffuse, (0, 1), 'diffuse')
        self._diffuse = diffuse

    @property
    def specular(self) -> float:  # numpydoc ignore=RT01
        """Return or set the specular value.

        Specular lighting simulates the bright spot of a light that appears
        on shiny objects. Must be between 0 and 1.

        Examples
        --------
        Set the global specular value to ``0.1``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.specular = 0.1
        >>> pv.global_theme.lighting_params.specular
        0.1

        """
        return self._specular

    @specular.setter
    def specular(self, specular: float):
        _check_range(specular, (0, 1), 'specular')
        self._specular = specular

    @property
    def specular_power(self) -> float:  # numpydoc ignore=RT01
        """Return or set the specular power value.

        Must be between 0.0 and 128.0.

        Examples
        --------
        Set the global specular power value to ``50``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.specular_power = 50
        >>> pv.global_theme.lighting_params.specular_power
        50

        """
        return self._specular_power

    @specular_power.setter
    def specular_power(self, specular_power: float):
        _check_range(specular_power, (0, 128), 'specular_power')
        self._specular_power = specular_power

    @property
    def emissive(self) -> bool:  # numpydoc ignore=RT01
        """Return or set if emissive is used with point Gaussian style.

        Examples
        --------
        Globally enable emissive lighting when using the point Gaussian style.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.emissive = True
        >>> pv.global_theme.lighting_params.emissive
        True

        """
        return self._emissive

    @emissive.setter
    def emissive(self, emissive: bool):
        self._emissive = bool(emissive)


class _DepthPeelingConfig(_ConfigBase):
    """PyVista depth peeling configuration.

    Examples
    --------
    Set global depth peeling parameters.

    >>> import pyvista as pv
    >>> pv.global_theme.depth_peeling.number_of_peels = 1
    >>> pv.global_theme.depth_peeling.occlusion_ratio = 0.0
    >>> pv.global_theme.depth_peeling.enabled = True

    """

    __slots__ = ['_enabled', '_number_of_peels', '_occlusion_ratio']

    def __init__(self):
        self._number_of_peels = 4
        self._occlusion_ratio = 0.0
        self._enabled = False

    @property
    def number_of_peels(self) -> int:  # numpydoc ignore=RT01
        """Return or set the number of peels in depth peeling.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.depth_peeling.number_of_peels = 1

        """
        return self._number_of_peels

    @number_of_peels.setter
    def number_of_peels(self, number_of_peels: int):
        self._number_of_peels = int(number_of_peels)

    @property
    def occlusion_ratio(self) -> float:  # numpydoc ignore=RT01
        """Return or set the occlusion ratio in depth peeling.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.depth_peeling.occlusion_ratio = 0.0

        """
        return self._occlusion_ratio

    @occlusion_ratio.setter
    def occlusion_ratio(self, occlusion_ratio: float):
        self._occlusion_ratio = float(occlusion_ratio)

    @property
    def enabled(self) -> bool:  # numpydoc ignore=RT01
        """Return or set if depth peeling is enabled.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.depth_peeling.enabled = True

        """
        return self._enabled

    @enabled.setter
    def enabled(self, enabled: bool):
        self._enabled = bool(enabled)

    def __repr__(self):
        txt = ['']
        parm = {
            'Number': 'number_of_peels',
            'Occlusion ratio': 'occlusion_ratio',
            'Enabled': 'enabled',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')
        return '\n'.join(txt)


class _SilhouetteConfig(_ConfigBase):
    """PyVista silhouette configuration.

    Examples
    --------
    Set global silhouette parameters.

    >>> import pyvista as pv
    >>> pv.global_theme.silhouette.enabled = True
    >>> pv.global_theme.silhouette.color = 'grey'
    >>> pv.global_theme.silhouette.line_width = 2
    >>> pv.global_theme.silhouette.feature_angle = 20

    """

    __slots__ = [
        '_color',
        '_decimate',
        '_enabled',
        '_feature_angle',
        '_line_width',
        '_opacity',
    ]

    def __init__(self):
        self._color = Color('black')
        self._line_width = 2
        self._opacity = 1.0
        self._feature_angle = None
        self._decimate = None
        self._enabled = False

    @property
    def enabled(self) -> bool:  # numpydoc ignore=RT01
        """Return or set whether silhouette is on or off."""
        return self._enabled

    @enabled.setter
    def enabled(self, enabled: bool):
        self._enabled = bool(enabled)

    @property
    def color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the silhouette color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.color = 'red'

        """
        return self._color

    @color.setter
    def color(self, color: ColorLike):
        self._color = Color(color)

    @property
    def line_width(self) -> float:  # numpydoc ignore=RT01
        """Return or set the silhouette line width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.line_width = 2.0

        """
        return self._line_width

    @line_width.setter
    def line_width(self, line_width: float):
        self._line_width = float(line_width)  # type: ignore[assignment]

    @property
    def opacity(self) -> float:  # numpydoc ignore=RT01
        """Return or set the silhouette opacity.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.opacity = 1.0

        """
        return self._opacity

    @opacity.setter
    def opacity(self, opacity: float):
        _check_range(opacity, (0, 1), 'opacity')
        self._opacity = float(opacity)

    @property
    def feature_angle(self) -> float | None:  # numpydoc ignore=RT01
        """Return or set the silhouette feature angle.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.feature_angle = 20.0

        """
        return self._feature_angle

    @feature_angle.setter
    def feature_angle(self, feature_angle: float | None):
        self._feature_angle = feature_angle

    @property
    def decimate(self) -> float:  # numpydoc ignore=RT01
        """Return or set the amount to decimate the silhouette.

        Parameter must be between 0 and 1.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.decimate = 0.9

        """
        return self._decimate  # type: ignore[return-value]

    @decimate.setter
    def decimate(self, decimate: float | None):
        if decimate is None:
            self._decimate = None
        else:
            _check_range(decimate, (0, 1), 'decimate')
            self._decimate = float(decimate)

    def __repr__(self):
        txt = ['']
        parm = {
            'Color': 'color',
            'Line width': 'line_width',
            'Opacity': 'opacity',
            'Feature angle': 'feature_angle',
            'Decimate': 'decimate',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')
        return '\n'.join(txt)


class _ColorbarConfig(_ConfigBase):
    """PyVista colorbar configuration.

    Examples
    --------
    Set the colorbar width.

    >>> import pyvista as pv
    >>> pv.global_theme.colorbar_horizontal.width = 0.2

    """

    __slots__ = ['_height', '_position_x', '_position_y', '_width']

    def __init__(self):
        self._width = None
        self._height = None
        self._position_x = None
        self._position_y = None

    @property
    def width(self) -> float:  # numpydoc ignore=RT01
        """Return or set colorbar width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.width = 0.2

        """
        return self._width  # type: ignore[return-value]

    @width.setter
    def width(self, width: float):
        self._width = float(width)

    @property
    def height(self) -> float:  # numpydoc ignore=RT01
        """Return or set colorbar height.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.height = 0.2

        """
        return self._height  # type: ignore[return-value]

    @height.setter
    def height(self, height: float):
        self._height = float(height)

    @property
    def position_x(self) -> float:  # numpydoc ignore=RT01
        """Return or set colorbar x position.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.position_x = 0.2

        """
        return self._position_x  # type: ignore[return-value]

    @position_x.setter
    def position_x(self, position_x: float):
        self._position_x = float(position_x)

    @property
    def position_y(self) -> float:  # numpydoc ignore=RT01
        """Return or set colorbar y position.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.position_y = 0.2

        """
        return self._position_y  # type: ignore[return-value]

    @position_y.setter
    def position_y(self, position_y: float):
        self._position_y = float(position_y)

    def __repr__(self):
        txt = ['']
        parm = {
            'Width': 'width',
            'Height': 'height',
            'X Position': 'position_x',
            'Y Position': 'position_y',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')

        return '\n'.join(txt)


class _AxesConfig(_ConfigBase):
    """PyVista axes configuration.

    Examples
    --------
    Show the default axes configuration values.

    >>> import pyvista as pv
    >>> pv.global_theme.axes.x_color
    Color(name='tomato', hex='#ff6347ff', opacity=255)

    >>> pv.global_theme.axes.y_color
    Color(name='sea_green', hex='#2e8b57ff', opacity=255)

    >>> pv.global_theme.axes.z_color
    Color(name='blue', hex='#0000ffff', opacity=255)

    >>> pv.global_theme.axes.box
    False

    >>> pv.global_theme.axes.show
    True

    Set the x-axis color to black.

    >>> pv.global_theme.axes.x_color = 'black'

    Show the axes orientation widget by default.

    >>> pv.global_theme.axes.show = True

    Use the :func:`axes orientation box <pyvista.create_axes_orientation_box>`
    as the orientation widget.

    >>> pv.global_theme.axes.box = True

    """

    __slots__ = ['_box', '_show', '_x_color', '_y_color', '_z_color']

    def __init__(self):
        self._x_color = Color('tomato')
        self._y_color = Color('seagreen')
        self._z_color = Color('mediumblue')
        self._box = False
        self._show = True

    def __repr__(self):
        txt = ['Axes configuration']
        parm = {
            'X Color': 'x_color',
            'Y Color': 'y_color',
            'Z Color': 'z_color',
            'Use Box': 'box',
            'Show': 'show',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')

        return '\n'.join(txt)

    @property
    def x_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set x-axis color.

        Examples
        --------
        Get the default x-axis color

        >>> import pyvista as pv
        >>> pv.global_theme.axes.x_color
        Color(name='tomato', hex='#ff6347ff', opacity=255)

        Change the default color.

        >>> pv.global_theme.axes.x_color = 'red'

        """
        return self._x_color

    @x_color.setter
    def x_color(self, color: ColorLike):
        self._x_color = Color(color)

    @property
    def y_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set y-axis color.

        Examples
        --------
        Get the default y-axis color

        >>> import pyvista as pv
        >>> pv.global_theme.axes.y_color
        Color(name='sea_green', hex='#2e8b57ff', opacity=255)

        Change the default color.

        >>> pv.global_theme.axes.y_color = 'green'

        """
        return self._y_color

    @y_color.setter
    def y_color(self, color: ColorLike):
        self._y_color = Color(color)

    @property
    def z_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set z-axis color.

        Examples
        --------
        Get the default z-axis color

        >>> import pyvista as pv
        >>> pv.global_theme.axes.z_color
        Color(name='blue', hex='#0000ffff', opacity=255)

        Change the default color.

        >>> pv.global_theme.axes.z_color = 'purple'

        """
        return self._z_color

    @z_color.setter
    def z_color(self, color: ColorLike):
        self._z_color = Color(color)

    @property
    def box(self) -> bool:  # numpydoc ignore=RT01
        """Use a box axes orientation widget.

        If ``True``, Use the :func:`axes orientation box <pyvista.create_axes_orientation_box>`
        instead of the :class:`pyvista.AxesActor` as the orientation widget for plots.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.axes.box = True

        """
        return self._box

    @box.setter
    def box(self, box: bool):
        self._box = bool(box)

    @property
    def show(self) -> bool:  # numpydoc ignore=RT01
        """Show or hide the axes actor.

        Examples
        --------
        Hide the axes by default.

        >>> import pyvista as pv
        >>> pv.global_theme.axes.show = False

        """
        return self._show

    @show.setter
    def show(self, show: bool):
        self._show = bool(show)


class _Font(_ConfigBase):
    """PyVista plotter font configuration.

    Examples
    --------
    Set the default font family to 'arial'.  Must be either
    'arial', 'courier', or 'times'.

    >>> import pyvista as pv
    >>> pv.global_theme.font.family = 'arial'

    Set the default font size to 20.

    >>> pv.global_theme.font.size = 20

    Set the default title size to 40

    >>> pv.global_theme.font.title_size = 40

    Set the default label size to 10

    >>> pv.global_theme.font.label_size = 10

    Set the default text color to 'grey'

    >>> pv.global_theme.font.color = 'grey'

    Set the string formatter used to format numerical data to '{:.6e}'

    >>> pv.global_theme.font.fmt = '{:.6e}'

    """

    __slots__ = ['_color', '_family', '_fmt', '_label_size', '_size', '_title_size']

    def __init__(self):
        self._family = 'arial'
        self._size = 12
        self._title_size = None
        self._label_size = None
        self._color = Color('white')
        self._fmt = None

    def __repr__(self):
        txt = ['']
        parm = {
            'Family': 'family',
            'Size': 'size',
            'Title size': 'title_size',
            'Label size': 'label_size',
            'Color': 'color',
            'Float format': 'fmt',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')

        return '\n'.join(txt)

    @property
    def family(self) -> str:  # numpydoc ignore=RT01
        """Return or set the font family.

        Must be one of the following:

        * ``"arial"``
        * ``"courier"``
        * ``"times"``

        Examples
        --------
        Set the default global font family to 'courier'.

        >>> import pyvista as pv
        >>> pv.global_theme.font.family = 'courier'

        """
        return self._family

    @family.setter
    def family(self, family: str):
        parse_font_family(family)  # check valid font
        self._family = family

    @property
    def size(self) -> int:  # numpydoc ignore=RT01
        """Return or set the font size.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.font.size = 20

        """
        return self._size

    @size.setter
    def size(self, size: int):
        self._size = int(size)

    @property
    def title_size(self) -> int:  # numpydoc ignore=RT01
        """Return or set the title size.

        If ``None``, then VTK uses ``UnconstrainedFontSizeOn`` for titles.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.font.title_size = 20

        """
        return self._title_size  # type: ignore[return-value]

    @title_size.setter
    def title_size(self, title_size: int | None):
        if title_size is None:
            self._title_size = None
        else:
            self._title_size = int(title_size)

    @property
    def label_size(self) -> int:  # numpydoc ignore=RT01
        """Return or set the label size.

        If ``None``, then VTK uses ``UnconstrainedFontSizeOn`` for labels.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.font.label_size = 20

        """
        return self._label_size  # type: ignore[return-value]

    @label_size.setter
    def label_size(self, label_size: int | None):
        if label_size is None:
            self._label_size = None
        else:
            self._label_size = int(label_size)

    @property
    def color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the font color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.font.color = 'black'

        """
        return self._color

    @color.setter
    def color(self, color: ColorLike):
        self._color = Color(color)

    @property
    def fmt(self) -> str:  # numpydoc ignore=RT01
        """Return or set the string formatter used to format numerical data.

        Examples
        --------
        Set the string formatter used to format numerical data to '{:.6e}'.

        >>> import pyvista as pv
        >>> pv.global_theme.font.fmt = '{:.6e}'

        """
        return self._fmt  # type: ignore[return-value]

    @fmt.setter
    def fmt(self, fmt: str):
        self._fmt = fmt


class _SliderStyleConfig(_ConfigBase):
    """PyVista configuration for a single slider style."""

    __slots__ = [
        '_cap_length',
        '_cap_opacity',
        '_cap_width',
        '_name',
        '_slider_color',
        '_slider_length',
        '_slider_width',
        '_tube_color',
        '_tube_width',
    ]

    def __init__(self):
        """Initialize the slider style configuration."""
        self._name = None
        self._slider_length = None
        self._slider_width = None
        self._slider_color = None
        self._tube_width = None
        self._tube_color = None
        self._cap_opacity = None
        self._cap_length = None
        self._cap_width = None

    @property
    def name(self) -> str:  # numpydoc ignore=RT01
        """Return the name of the slider style configuration."""
        return self._name  # type: ignore[return-value]

    @name.setter
    def name(self, name: str):
        self._name = name

    @property
    def cap_width(self) -> float:  # numpydoc ignore=RT01
        """Return or set the cap width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.cap_width = 0.02

        """
        return self._cap_width  # type: ignore[return-value]

    @cap_width.setter
    def cap_width(self, cap_width: float):
        self._cap_width = float(cap_width)

    @property
    def cap_length(self) -> float:  # numpydoc ignore=RT01
        """Return or set the cap length.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.cap_length = 0.01

        """
        return self._cap_length  # type: ignore[return-value]

    @cap_length.setter
    def cap_length(self, cap_length: float):
        self._cap_length = float(cap_length)

    @property
    def cap_opacity(self) -> float:  # numpydoc ignore=RT01
        """Return or set the cap opacity.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.cap_opacity = 1.0

        """
        return self._cap_opacity  # type: ignore[return-value]

    @cap_opacity.setter
    def cap_opacity(self, cap_opacity: float):
        _check_range(cap_opacity, (0, 1), 'cap_opacity')
        self._cap_opacity = float(cap_opacity)

    @property
    def tube_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the tube color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.tube_color = 'black'

        """
        return self._tube_color  # type: ignore[return-value]

    @tube_color.setter
    def tube_color(self, tube_color: ColorLike):
        self._tube_color = Color(tube_color)

    @property
    def tube_width(self) -> float:  # numpydoc ignore=RT01
        """Return or set the tube_width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.tube_width = 0.005

        """
        return self._tube_width  # type: ignore[return-value]

    @tube_width.setter
    def tube_width(self, tube_width: float):
        self._tube_width = float(tube_width)

    @property
    def slider_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the slider color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.slider_color = 'grey'

        """
        return self._slider_color  # type: ignore[return-value]

    @slider_color.setter
    def slider_color(self, slider_color: ColorLike):
        self._slider_color = Color(slider_color)

    @property
    def slider_width(self) -> float:  # numpydoc ignore=RT01
        """Return or set the slider width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.slider_width = 0.04

        """
        return self._slider_width  # type: ignore[return-value]

    @slider_width.setter
    def slider_width(self, slider_width: float):
        self._slider_width = float(slider_width)

    @property
    def slider_length(self) -> float:  # numpydoc ignore=RT01
        """Return or set the slider_length.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.slider_length = 0.02

        """
        return self._slider_length  # type: ignore[return-value]

    @slider_length.setter
    def slider_length(self, slider_length: float):
        self._slider_length = float(slider_length)

    def __repr__(self):
        txt = ['']
        parm = {
            'Slider length': 'slider_length',
            'Slider width': 'slider_width',
            'Slider color': 'slider_color',
            'Tube width': 'tube_width',
            'Tube color': 'tube_color',
            'Cap opacity': 'cap_opacity',
            'Cap length': 'cap_length',
            'Cap width': 'cap_width',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'        {name:<17}: {setting}')
        return '\n'.join(txt)


class _SliderConfig(_ConfigBase):
    """PyVista configuration encompassing all slider styles.

    Examples
    --------
    Set the classic slider configuration.

    >>> import pyvista as pv
    >>> slider_styles = pv.global_theme.slider_styles
    >>> slider_styles.classic.slider_length = 0.02
    >>> slider_styles.classic.slider_width = 0.04
    >>> slider_styles.classic.slider_color = (0.5, 0.5, 0.5)
    >>> slider_styles.classic.tube_width = 0.005
    >>> slider_styles.classic.tube_color = (1.0, 1.0, 1.0)
    >>> slider_styles.classic.cap_opacity = 1
    >>> slider_styles.classic.cap_length = 0.01
    >>> slider_styles.classic.cap_width = 0.02

    Set the modern slider configuration.

    >>> slider_styles.modern.slider_length = 0.02
    >>> slider_styles.modern.slider_width = 0.04
    >>> slider_styles.modern.slider_color = (0.43, 0.44, 0.45)
    >>> slider_styles.modern.tube_width = 0.04
    >>> slider_styles.modern.tube_color = (0.69, 0.70, 0.709)
    >>> slider_styles.modern.cap_opacity = 0
    >>> slider_styles.modern.cap_length = 0.01
    >>> slider_styles.modern.cap_width = 0.02

    """

    __slots__ = ['_classic', '_modern']

    def __init__(self):
        """Initialize the slider configuration."""
        self._classic = _SliderStyleConfig()
        self._classic.name = 'classic'
        self._classic.slider_length = 0.02
        self._classic.slider_width = 0.04
        self._classic.slider_color = 'gray'
        self._classic.tube_width = 0.005
        self._classic.tube_color = 'white'
        self._classic.cap_opacity = 1
        self._classic.cap_length = 0.01
        self._classic.cap_width = 0.02

        self._modern = _SliderStyleConfig()
        self._modern.name = 'modern'
        self._modern.slider_length = 0.02
        self._modern.slider_width = 0.04
        self._modern.slider_color = (110, 113, 117)
        self._modern.tube_width = 0.04
        self._modern.tube_color = (178, 179, 181)
        self._modern.cap_opacity = 0
        self._modern.cap_length = 0.01
        self._modern.cap_width = 0.02

    @property
    def classic(self) -> _SliderStyleConfig:  # numpydoc ignore=RT01
        """Return the Classic slider configuration."""
        return self._classic

    @classic.setter
    def classic(self, config: _SliderStyleConfig):
        if not isinstance(config, _SliderStyleConfig):
            msg = 'Configuration type must be `_SliderStyleConfig`'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._classic = config

    @property
    def modern(self) -> _SliderStyleConfig:  # numpydoc ignore=RT01
        """Return the Modern slider configuration."""
        return self._modern

    @modern.setter
    def modern(self, config: _SliderStyleConfig):
        if not isinstance(config, _SliderStyleConfig):
            msg = 'Configuration type must be `_SliderStyleConfig`'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._modern = config

    def __repr__(self):
        txt = ['']
        parm = {
            'Classic': 'classic',
            'Modern': 'modern',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')
        return '\n'.join(txt)

    def __iter__(self):
        for style in [self._classic, self._modern]:
            yield style.name


class _TrameConfig(_ConfigBase):
    """PyVista Trame configuration.

    Examples
    --------
    Set global trame view parameters.

    >>> import pyvista as pv
    >>> pv.global_theme.trame.interactive_ratio = 2
    >>> pv.global_theme.trame.still_ratio = 2

    """

    __slots__ = [
        '_default_mode',
        '_interactive_ratio',
        '_jupyter_extension_available',
        '_jupyter_extension_enabled',
        '_jupyter_server_name',
        '_jupyter_server_port',
        '_server_proxy_enabled',
        '_server_proxy_prefix',
        '_still_ratio',
    ]

    def __init__(self):
        self._interactive_ratio = 1
        self._still_ratio = 1
        self._jupyter_server_name = 'pyvista-jupyter'
        self._jupyter_server_port = 0
        self._server_proxy_enabled = 'PYVISTA_TRAME_SERVER_PROXY_PREFIX' in os.environ
        # default for ``jupyter-server-proxy``
        service = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '')
        prefix = os.environ.get('PYVISTA_TRAME_SERVER_PROXY_PREFIX', '/proxy/')
        if service and not prefix.startswith('http'):
            # JupyterHub service prefixes are URL paths, not filesystem paths,
            # so use PurePosixPath to force forward-slash joining on Windows.
            self._server_proxy_prefix = (
                str(pathlib.PurePosixPath(service) / prefix.lstrip('/')).rstrip('/') + '/'
            )
            self._server_proxy_enabled = True
        else:
            self._server_proxy_prefix = prefix
        self._jupyter_extension_available = 'TRAME_JUPYTER_WWW' in os.environ
        self._jupyter_extension_enabled = (
            self._jupyter_extension_available and not self._server_proxy_enabled
        )
        # if set, jupyter_mode overwrites defaults
        jupyter_mode = os.environ.get('PYVISTA_TRAME_JUPYTER_MODE')
        if jupyter_mode == 'extension' and self._jupyter_extension_available:  # pragma: no cover
            self._server_proxy_enabled = False
            self._jupyter_extension_enabled = True
        elif jupyter_mode == 'proxy' and self._server_proxy_enabled:  # pragma: no cover
            self._jupyter_extension_enabled = False
        elif jupyter_mode == 'native':  # pragma: no cover
            self._jupyter_extension_enabled = False
            self._server_proxy_enabled = False
        self._default_mode = 'trame'

    @property
    def interactive_ratio(self) -> float:  # numpydoc ignore=RT01
        """Return or set the interactive ratio for PyVista Trame views.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.trame.interactive_ratio = 2

        """
        return self._interactive_ratio

    @interactive_ratio.setter
    def interactive_ratio(self, interactive_ratio: float):
        self._interactive_ratio = interactive_ratio  # type: ignore[assignment]

    @property
    def still_ratio(self) -> float:  # numpydoc ignore=RT01
        """Return or set the still ratio for PyVista Trame views.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.trame.still_ratio = 2

        """
        return self._still_ratio

    @still_ratio.setter
    def still_ratio(self, still_ratio: float):
        self._still_ratio = still_ratio  # type: ignore[assignment]

    @property
    def jupyter_server_name(self):  # numpydoc ignore=RT01
        """Return or set the trame server name PyVista uses in Jupyter.

        This defaults to ``'pyvista-jupyter'``.

        This must be set before running :func:`pyvista.set_jupyter_backend`
        to ensure a server of this name is launched.

        Most users should not need to modify this.

        """
        return self._jupyter_server_name

    @jupyter_server_name.setter
    def jupyter_server_name(self, name: str):
        self._jupyter_server_name = name

    @property
    def jupyter_server_port(self) -> int:  # numpydoc ignore=RT01
        """Return or set the port for the Trame Jupyter server."""
        return self._jupyter_server_port

    @jupyter_server_port.setter
    def jupyter_server_port(self, port: int):
        self._jupyter_server_port = port

    @property
    def server_proxy_enabled(self) -> bool:  # numpydoc ignore=RT01
        """Return or set if use of relative URLs is enabled for the Jupyter interface."""
        return self._server_proxy_enabled

    @server_proxy_enabled.setter
    def server_proxy_enabled(self, enabled: bool):
        if enabled and self.jupyter_extension_enabled:
            warn_external('Enabling server_proxy will disable jupyter_extension')
            self._jupyter_extension_enabled = False

        self._server_proxy_enabled = bool(enabled)

    @property
    def server_proxy_prefix(self):  # numpydoc ignore=RT01
        """Return or set URL prefix when using relative URLs with the Jupyter interface."""
        return self._server_proxy_prefix

    @server_proxy_prefix.setter
    def server_proxy_prefix(self, prefix: str):
        self._server_proxy_prefix = prefix

    @property
    def jupyter_extension_available(self) -> bool:  # numpydoc ignore=RT01
        """Return whether the trame_jupyter_extension is detected."""
        return self._jupyter_extension_available

    @jupyter_extension_available.setter
    def jupyter_extension_available(self, _available: bool):
        warn_external(
            'The jupyter_extension_available flag is read only and is automatically detected.'
        )

    @property
    def jupyter_extension_enabled(self) -> bool:  # numpydoc ignore=RT01
        """Return or set whether to use the trame_jupyter_extension to communicate with clients."""
        return self._jupyter_extension_enabled

    @jupyter_extension_enabled.setter
    def jupyter_extension_enabled(self, enabled: bool):
        if enabled and not self.jupyter_extension_available:
            msg = 'The trame_jupyter_extension is not available'
            raise ValueError(msg)

        if enabled and self.server_proxy_enabled:
            warn_external('Enabling jupyter_extension will disable server_proxy')
            self._server_proxy_enabled = False

        self._jupyter_extension_enabled = bool(enabled)

    @property
    def default_mode(self):  # numpydoc ignore=RT01
        """Return or set the default mode of the Trame backend.

        * ``'trame'``: Uses a view that can switch between client and server
          rendering modes.
        * ``'server'``: Uses a view that is purely server rendering.
        * ``'client'``: Uses a view that is purely client rendering (generally
          safe without a virtual frame buffer)

        """
        return self._default_mode

    @default_mode.setter
    def default_mode(self, mode: str):
        self._default_mode = mode


class _CameraConfig(_ConfigBase):
    """PyVista camera configuration.

    Examples
    --------
    Set global camera parameters.

    >>> import pyvista as pv
    >>> pv.global_theme.camera.position = [1.0, 1.0, 1.0]
    >>> pv.global_theme.camera.viewup = [0.0, 0.0, 1.0]

    """

    __slots__ = [
        '_parallel_projection',
        '_parallel_scale',
        '_position',
        '_viewup',
    ]

    def __init__(self):
        self._position = [1.0, 1.0, 1.0]
        self._viewup = [0.0, 0.0, 1.0]
        self._parallel_projection = False
        self._parallel_scale = 1.0

    @property
    def position(self) -> VectorLike[float]:  # numpydoc ignore=RT01
        """Return or set the camera position.

        Examples
        --------
        Set camera position.

        >>> import pyvista as pv
        >>> pv.global_theme.camera.position = [1.0, 1.0, 1.0]

        """
        return self._position

    @position.setter
    def position(self, position: VectorLike[float]):
        self._position = position  # type: ignore[assignment]

    @property
    def viewup(self) -> VectorLike[float]:  # numpydoc ignore=RT01
        """Return or set the camera viewup.

        Examples
        --------
        Set camera viewup.

        >>> import pyvista as pv
        >>> pv.global_theme.camera.viewup = [0.0, 0.0, 1.0]

        """
        return self._viewup

    @viewup.setter
    def viewup(self, viewup: VectorLike[float]):
        self._viewup = viewup  # type: ignore[assignment]

    @property
    def parallel_projection(self) -> bool:  # numpydoc ignore=RT01
        """Return or set parallel projection mode.

        Examples
        --------
        Enable parallel projection.

        >>> import pyvista as pv
        >>> pv.global_theme.camera.parallel_projection = True

        """
        return self._parallel_projection

    @parallel_projection.setter
    def parallel_projection(self, value: bool) -> None:
        self._parallel_projection = value

    @property
    def parallel_scale(self) -> bool:  # numpydoc ignore=RT01
        """Return or set parallel scale.

        Examples
        --------
        Set parallel scale.

        >>> import pyvista as pv
        >>> pv.global_theme.camera.parallel_scale = 2.0

        """
        return self._parallel_scale  # type: ignore[return-value]

    @parallel_scale.setter
    def parallel_scale(self, value: bool) -> None:
        self._parallel_scale = value


class _PlotCellConfig(_ConfigBase):
    """Internal config for plotting cells."""

    __slots__ = ['_font_size', '_line_width', '_normals_scale', '_point_size']

    def __init__(self):
        self._line_width = 5
        self._point_size = 30
        self._font_size = 20
        self._normals_scale = 0.1


class Theme(_ConfigBase):
    """Base VTK theme.

    Notes
    -----
    This section is aimed at theme authors and plugin package
    maintainers; end users calling :func:`~pyvista.set_plot_theme` do
    not need any of it.

    Subclasses that declare a class-level ``_default_name`` are
    automatically registered by that name via ``__init_subclass__``
    and become available through :func:`~pyvista.set_plot_theme`, the
    ``PYVISTA_PLOT_THEME`` environment variable, and
    :func:`~pyvista.registered_themes`. Subclasses without
    ``_default_name`` are not registered, so ad-hoc subclasses remain
    a valid pattern.

    For plugin packages distributing themes, the recommended path is to
    declare a ``pyvista.themes`` entry point so the theme is discovered
    without requiring users to import the package first:

    .. code-block:: toml

        [project.entry-points.'pyvista.themes']
        my_theme = 'my_package.theme:MyTheme'

    Subclass-based auto-registration requires the defining module to be
    imported before the name resolves, so it is primarily useful for
    scripts, notebooks, testing, and local development. Plugin packages
    should prefer the entry-point path.

    Examples
    --------
    Change the global default background color to white.

    >>> import pyvista as pv
    >>> pv.global_theme.color = 'white'

    Show edges by default.

    >>> pv.global_theme.show_edges = True

    Create a new theme from the default theme and apply it globally.

    >>> from pyvista.plotting.themes import DocumentTheme
    >>> my_theme = DocumentTheme()
    >>> my_theme.color = 'red'
    >>> my_theme.background = 'white'
    >>> pv.global_theme.load_theme(my_theme)

    Define a custom theme that auto-registers under a name.

    >>> from typing import ClassVar
    >>> class MyTheme(DocumentTheme):
    ...     _default_name: ClassVar[str] = 'my_theme'
    >>> pv.set_plot_theme('my_theme')  # doctest: +SKIP

    """

    # ``_plot_cell`` is an internal-only sub-config — exclude it from
    # ``to_dict`` output so themes serialize/deserialize round-trip cleanly.
    _TO_DICT_SKIP: ClassVar[frozenset[str]] = frozenset({'plot_cell'})

    _default_name: ClassVar[str | None] = None

    def __init_subclass__(cls, **kwargs: Any) -> None:
        """Auto-register ``Theme`` subclasses by ``_default_name``."""
        super().__init_subclass__(**kwargs)
        # Read from __dict__ directly so inherited _default_name does not
        # accidentally re-register a parent theme's name.
        if '_default_name' not in cls.__dict__:
            # Subclass does not opt into name-based discovery. Silent skip —
            # ad-hoc subclasses are a valid pattern.
            return
        name = cls.__dict__['_default_name']
        if not isinstance(name, str) or not name:
            warn_external(
                f'Theme subclass {cls.__module__}.{cls.__qualname__} declared '
                f"an invalid '_default_name' ({name!r}); expected a non-empty "
                'string. The subclass will not be discoverable by name.',
            )
            return
        _register_theme_class(
            name,
            cls,
            source=f'{cls.__module__}.{cls.__qualname__}',
        )

    __slots__ = [
        '_above_range_color',
        '_allow_empty_mesh',
        '_anti_aliasing',
        '_auto_close',
        '_axes',
        '_background',
        '_before_close_callback',
        '_before_close_callback',
        '_below_range_color',
        '_camera',
        '_cmap',
        '_color',
        '_color_cycler',
        '_colorbar_horizontal',
        '_colorbar_orientation',
        '_colorbar_vertical',
        '_depth_peeling',
        '_edge_color',
        '_edge_opacity',
        '_enable_camera_orientation_widget',
        '_floor_color',
        '_font',
        '_full_screen',
        '_hidden_line_removal',
        '_image_scale',
        '_interactive',
        '_interactor_style',
        '_interpolate_before_map',
        '_jupyter_backend',
        '_lighting',
        '_lighting_params',
        '_line_width',
        '_logo_file',
        '_multi_rendering_splitting_position',
        '_multi_samples',
        '_name',
        '_nan_color',
        '_notebook',
        '_opacity',
        '_outline_color',
        '_plot_cell',
        '_point_shape',
        '_point_size',
        '_render_lines_as_tubes',
        '_render_points_as_spheres',
        '_resample_environment_texture',
        '_return_cpos',
        '_sharp_edges_feature_angle',
        '_show_edges',
        '_show_scalar_bar',
        '_show_vertices',
        '_silhouette',
        '_slider_styles',
        '_smooth_shading',
        '_split_sharp_edges',
        '_title',
        '_trame',
        '_transparent_background',
        '_volume_mapper',
        '_window_size',
    ]

    def __init__(self):
        """Initialize the theme."""
        self._name = type(self)._default_name or 'default'
        self._background = Color([0.3, 0.3, 0.3])
        self._full_screen = False
        self._camera = _CameraConfig()

        self._notebook = None
        self._window_size = [1024, 768]
        self._image_scale = 1
        self._font = _Font()
        self._cmap: ColormapOptions = 'viridis'
        self._color = Color('white')
        self._color_cycler = None
        self._nan_color = Color('darkgray')
        self._above_range_color = Color('grey')
        self._below_range_color = Color('grey')
        self._edge_color = Color('black')
        self._line_width = 1.0
        self._point_size = 5.0
        self._outline_color = Color('white')
        self._floor_color = Color('gray')
        self._colorbar_orientation = 'horizontal'

        self._colorbar_horizontal = _ColorbarConfig()
        self._colorbar_horizontal.width = 0.6
        self._colorbar_horizontal.height = 0.08
        self._colorbar_horizontal.position_x = 0.35
        self._colorbar_horizontal.position_y = 0.05

        self._colorbar_vertical = _ColorbarConfig()
        self._colorbar_vertical.width = 0.08
        self._colorbar_vertical.height = 0.45
        self._colorbar_vertical.position_x = 0.9
        self._colorbar_vertical.position_y = 0.02

        self._show_scalar_bar = True
        self._show_edges = False
        self._show_vertices = False
        self._lighting = True
        self._interactive = False
        self._interactor_style = 'trackball_style'
        self._render_points_as_spheres = False
        self._render_lines_as_tubes = False
        self._point_shape = None
        self._transparent_background = False
        self._title = 'PyVista'
        self._axes = _AxesConfig()
        self._split_sharp_edges = False
        self._sharp_edges_feature_angle = 30.0
        self._before_close_callback = None
        self._allow_empty_mesh = False

        # Grab system flag for anti-aliasing
        # Use a default value of 8 multi-samples as this is default for VTK
        try:
            self._multi_samples = int(os.environ.get('PYVISTA_MULTI_SAMPLES', '8'))
        except ValueError:  # pragma: no cover
            self._multi_samples = 8

        # Grab system flag for auto-closing
        self._auto_close = os.environ.get('PYVISTA_AUTO_CLOSE', '').lower() != 'false'

        self._jupyter_backend: str | None = os.environ.get('PYVISTA_JUPYTER_BACKEND')
        self._trame = _TrameConfig()

        self._multi_rendering_splitting_position = None
        self._volume_mapper = 'smart'
        self._smooth_shading = False
        self._depth_peeling = _DepthPeelingConfig()
        self._silhouette = _SilhouetteConfig()
        self._slider_styles = _SliderConfig()
        self._return_cpos = True
        self._hidden_line_removal = False
        self._anti_aliasing = 'msaa'
        self._enable_camera_orientation_widget = False

        self._lighting_params = _LightingConfig()
        self._interpolate_before_map = True
        self._opacity = 1.0
        self._edge_opacity = 1.0

        self._logo_file = None

        self._resample_environment_texture: bool | float = False

        self._plot_cell = _PlotCellConfig()

    @property
    def hidden_line_removal(self) -> bool:  # numpydoc ignore=RT01
        """Return or set hidden line removal.

        Wireframe geometry will be drawn using hidden line removal if
        the rendering engine supports it.

        See Also
        --------
        pyvista.Plotter.enable_hidden_line_removal

        Examples
        --------
        Enable hidden line removal.

        >>> import pyvista as pv
        >>> pv.global_theme.hidden_line_removal = True
        >>> pv.global_theme.hidden_line_removal
        True

        """
        return self._hidden_line_removal

    @hidden_line_removal.setter
    def hidden_line_removal(self, value: bool):
        self._hidden_line_removal = value

    @property
    def interpolate_before_map(self) -> bool:  # numpydoc ignore=RT01
        """Return or set whether to interpolate colors before mapping.

        If the ``interpolate_before_map`` is turned off, the color
        mapping occurs at polygon points and colors are interpolated,
        which is generally less accurate whereas if the
        ``interpolate_before_map`` is on (the default), then the scalars
        will be interpolated across the topology of the dataset which is
        more accurate.

        See also :ref:`interpolate_before_map_example`.

        Examples
        --------
        Enable hidden line removal.

        >>> import pyvista as pv

        Load a cylinder which has cells with a wide spread

        >>> cyl = pv.Cylinder(direction=(0, 0, 1), height=2).elevation()

        Common display argument to make sure all else is constant

        >>> dargs = dict(scalars='Elevation', cmap='rainbow', show_edges=True)

        >>> pl = pv.Plotter(shape=(1, 2))
        >>> _ = pl.add_mesh(
        ...     cyl,
        ...     interpolate_before_map=False,
        ...     scalar_bar_args={'title': 'Elevation - interpolated'},
        ...     **dargs,
        ... )
        >>> pl.subplot(0, 1)
        >>> _ = pl.add_mesh(
        ...     cyl,
        ...     interpolate_before_map=True,
        ...     scalar_bar_args={'title': 'Elevation - interpolated'},
        ...     **dargs,
        ... )
        >>> pl.link_views()
        >>> pl.camera_position = pv.CameraPosition(
        ...     position=(-1.67, -5.10, 2.06),
        ...     focal_point=(0.0, 0.0, 0.0),
        ...     viewup=(0.00, 0.37, 0.93),
        ... )
        >>> pl.show()  # doctest: +SKIP

        """
        return self._interpolate_before_map

    @interpolate_before_map.setter
    def interpolate_before_map(self, value: bool):
        self._interpolate_before_map = value

    @property
    def opacity(self) -> float:  # numpydoc ignore=RT01
        """Return or set the opacity.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.opacity = 0.5

        """
        return self._opacity

    @opacity.setter
    def opacity(self, opacity: float):
        _check_range(opacity, (0, 1), 'opacity')
        self._opacity = float(opacity)

    @property
    def edge_opacity(self) -> float:  # numpydoc ignore=RT01
        """Return or set the edges opacity.

        .. note::
            `edge_opacity` uses ``SetEdgeOpacity`` as the underlying method which
            requires VTK version 9.3 or higher. If ``SetEdgeOpacity`` is not
            available, `edge_opacity` is set to 1.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.edge_opacity = 0.5

        """
        return self._edge_opacity

    @edge_opacity.setter
    def edge_opacity(self, edge_opacity: float):
        _check_range(edge_opacity, (0, 1), 'edge_opacity')
        self._edge_opacity = float(edge_opacity)

    @property
    def above_range_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the default above range color.

        Examples
        --------
        Set the above range color to red.

        >>> import pyvista as pv
        >>> pv.global_theme.above_range_color = 'r'
        >>> pv.global_theme.above_range_color
        Color(name='red', hex='#ff0000ff', opacity=255)

        """
        return self._above_range_color

    @above_range_color.setter
    def above_range_color(self, value: ColorLike):
        self._above_range_color = Color(value)

    @property
    def below_range_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the default below range color.

        Examples
        --------
        Set the below range color to blue.

        >>> import pyvista as pv
        >>> pv.global_theme.below_range_color = 'b'
        >>> pv.global_theme.below_range_color
        Color(name='blue', hex='#0000ffff', opacity=255)

        """
        return self._below_range_color

    @below_range_color.setter
    def below_range_color(self, value: ColorLike):
        self._below_range_color = Color(value)

    @property
    def return_cpos(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default behavior of returning the camera position.

        Examples
        --------
        Disable returning camera position by ``show`` and ``plot`` methods.

        >>> import pyvista as pv
        >>> pv.global_theme.return_cpos = False

        """
        return self._return_cpos

    @return_cpos.setter
    def return_cpos(self, value: bool):
        self._return_cpos = value

    @property
    def background(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the default background color of pyvista plots.

        Examples
        --------
        Set the default global background of all plots to white.

        >>> import pyvista as pv
        >>> pv.global_theme.background = 'white'

        """
        return self._background

    @background.setter
    def background(self, new_background: ColorLike) -> None:
        self._background = Color(new_background)

    @property
    def jupyter_backend(
        self,
    ) -> str | None:  # numpydoc ignore=RT01
        """Return or set the jupyter notebook plotting backend.

        Jupyter backend to use when plotting.  Must be one of the
        following:

        * ``'static'`` : Display a single static image within the
          Jupyterlab environment.  Still requires that a virtual
          framebuffer be set up when displaying on a headless server,
          but does not require any additional modules to be installed.

        * ``'client'`` : Export/serialize the scene graph to be rendered
          with VTK.js client-side through ``trame``. Requires ``trame``
          and ``jupyter-server-proxy`` to be installed.

        * ``'server'``: Render remotely and stream the resulting VTK
          images back to the client using ``trame``. This replaces the
          ``'ipyvtklink'`` backend with better performance.
          Supports the most VTK features, but suffers from minor lag due
          to remote rendering. Requires that a virtual framebuffer be set
          up when displaying on a headless server. Must have at least ``trame``
          and ``jupyter-server-proxy`` installed for cloud/remote Jupyter
          instances. This mode is also aliased by ``'trame'``.

        * ``'trame'``: The full Trame-based backend that combines both
          ``'server'`` and ``'client'`` into one backend. This requires a
          virtual frame buffer.

        * ``'html'``: The ``'client'`` backend, but able to be embedded.

        * ``'none'`` : Do not display any plots within jupyterlab,
          instead display using dedicated VTK render windows.  This
          will generate nothing on headless servers even with a
          virtual framebuffer.

        Examples
        --------
        Just show static images.

        >>> pv.set_jupyter_backend('static')  # doctest:+SKIP

        Disable all plotting within JupyterLab and display using a
        standard desktop VTK render window.

        >>> pv.set_jupyter_backend('none')  # doctest:+SKIP

        Reset to auto-detect the best available backend.

        >>> pv.set_jupyter_backend(None)  # doctest:+SKIP

        """
        return self._jupyter_backend

    @jupyter_backend.setter
    def jupyter_backend(self, backend: str | None):
        from pyvista.jupyter import _validate_jupyter_backend  # noqa: PLC0415

        self._jupyter_backend = _validate_jupyter_backend(backend)

    @property
    def trame(self) -> _TrameConfig:  # numpydoc ignore=RT01
        """Return or set the default trame parameters."""
        return self._trame

    @trame.setter
    def trame(self, config: _TrameConfig):
        if not isinstance(config, _TrameConfig):
            msg = 'Configuration type must be `_TrameConfig`.'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._trame = config

    @property
    def auto_close(self) -> bool:  # numpydoc ignore=RT01
        """Automatically close the figures when finished plotting.

        .. DANGER::
           Set to ``False`` with extreme caution.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.auto_close = False

        """
        return self._auto_close

    @auto_close.setter
    def auto_close(self, value: bool):
        self._auto_close = value

    @property
    def full_screen(self) -> bool:  # numpydoc ignore=RT01
        """Return if figures are shown in full screen.

        Examples
        --------
        Set windows to be full screen by default.

        >>> import pyvista as pv
        >>> pv.global_theme.full_screen = True

        """
        return self._full_screen

    @full_screen.setter
    def full_screen(self, value: bool):
        self._full_screen = value

    @property
    def enable_camera_orientation_widget(self) -> bool:  # numpydoc ignore=RT01
        """Enable the camera orientation widget in all plotters.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.enable_camera_orientation_widget = True
        >>> pv.global_theme.enable_camera_orientation_widget
        True

        """
        return self._enable_camera_orientation_widget

    @enable_camera_orientation_widget.setter
    def enable_camera_orientation_widget(self, value: bool):
        self._enable_camera_orientation_widget = value

    @property
    def camera(self):  # numpydoc ignore=RT01
        """Return or set the default camera position.

        Examples
        --------
        Set both the position and viewup of the camera.

        >>> import pyvista as pv
        >>> pv.global_theme.camera.position = [1.0, 1.0, 1.0]
        >>> pv.global_theme.camera.viewup = [0.0, 0.0, 1.0]

        """
        return self._camera

    @camera.setter
    def camera(self, camera):
        if isinstance(camera, dict):
            self._camera = _CameraConfig.from_dict(camera)
        elif isinstance(camera, _CameraConfig):
            self._camera = camera
        else:
            msg = f'camera value must either be a `dict` or a `_CameraConfig`, got {type(camera)}'
            raise TypeError(msg)

    @property
    def notebook(self) -> bool | None:  # numpydoc ignore=RT01
        """Return or set the state of notebook plotting.

        Setting this to ``True`` always enables notebook plotting,
        while setting it to ``False`` disables plotting even when
        plotting within a jupyter notebook and plots externally.

        Examples
        --------
        Disable all jupyter notebook plotting.

        >>> import pyvista as pv
        >>> pv.global_theme.notebook = False

        """
        return self._notebook

    @notebook.setter
    def notebook(self, value: bool | None):
        self._notebook = value

    @property
    def window_size(self) -> list[int]:  # numpydoc ignore=RT01
        """Return or set the default render window size.

        Examples
        --------
        Set window size to ``[400, 400]``.

        >>> import pyvista as pv
        >>> pv.global_theme.window_size = [400, 400]

        """
        return self._window_size

    @window_size.setter
    def window_size(self, window_size: list[int]):
        if len(window_size) != 2:
            msg = 'Expected a length 2 iterable for ``window_size``.'
            raise ValueError(msg)

        # ensure positive size
        if window_size[0] < 0 or window_size[1] < 0:
            msg = 'Window size must be a positive value.'
            raise ValueError(msg)

        self._window_size = window_size

    @property
    def image_scale(self) -> int:  # numpydoc ignore=RT01
        """Return or set the default image scale factor."""
        return self._image_scale

    @image_scale.setter
    def image_scale(self, value: int):
        value = int(value)
        if value < 1:
            msg = 'Scale factor must be a positive integer.'
            raise ValueError(msg)
        self._image_scale = int(value)

    @property
    def font(self) -> _Font:  # numpydoc ignore=RT01
        """Return or set the default font size, family, and/or color.

        Examples
        --------
        Set the default font family to 'arial'.  Must be either
        'arial', 'courier', or 'times'.

        >>> import pyvista as pv
        >>> pv.global_theme.font.family = 'arial'

        Set the default font size to 20.

        >>> pv.global_theme.font.size = 20

        Set the default title size to 40.

        >>> pv.global_theme.font.title_size = 40

        Set the default label size to 10.

        >>> pv.global_theme.font.label_size = 10

        Set the default text color to 'grey'.

        >>> pv.global_theme.font.color = 'grey'

        String formatter used to format numerical data to '{:.6e}'.

        >>> pv.global_theme.font.fmt = '{:.6e}'

        """
        return self._font

    @font.setter
    def font(self, config: _Font):
        if not isinstance(config, _Font):
            msg = 'Configuration type must be `_Font`.'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._font = config

    @property
    def cmap(self) -> ColormapOptions:  # numpydoc ignore=RT01
        """Return or set the default colormap of pyvista.

        See :ref:`named_colormaps` for supported colormaps.
        Only applicable when displaying ``scalars``.

        You can also specify a list of colors to override an existing
        colormap with a custom one.  For example, to create a three
        color colormap you might specify ``['green', 'red', 'blue']``

        Examples
        --------
        Set the default global colormap to 'jet'.

        >>> import pyvista as pv
        >>> pv.global_theme.cmap = 'jet'

        """
        return self._cmap

    @cmap.setter
    def cmap(self, cmap: ColormapOptions):
        get_cmap_safe(cmap)  # for validation
        self._cmap = cmap

    @property
    def color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the default color of meshes in pyvista.

        Used for meshes without ``scalars``.

        When setting, the value must be either a string, rgb list,
        or hex color string.  For example:

        * ``color='white'``
        * ``color='w'``
        * ``color=[1.0, 1.0, 1.0]``
        * ``color='#FFFFFF'``

        Examples
        --------
        Set the default mesh color to 'red'.

        >>> import pyvista as pv
        >>> pv.global_theme.color = 'red'

        """
        return self._color

    @color.setter
    def color(self, color: ColorLike):
        self._color = Color(color)

    @property
    def color_cycler(self):  # numpydoc ignore=RT01
        """Return or set the default color cycler used to color meshes.

        This color cycler is iterated over by each renderer to sequentially
        color datasets when displaying them through ``add_mesh``.

        When setting, the value must be either a list of color-like objects,
        or a cycler of color-like objects. If the value passed is a single
        string, it must be one of:

            * ``'default'`` - Use the default color cycler (matches matplotlib's default)
            * ``'matplotlib`` - Dynamically get matplotlib's current theme's color cycler.
            * ``'all'`` - Cycle through all available colors in
              ``pyvista.plotting.colors.hexcolors``

        Setting to ``None`` will disable the use of the color cycler.

        Examples
        --------
        Set the default color cycler to iterate through red, green, and blue.

        >>> import pyvista as pv
        >>> pv.global_theme.color_cycler = ['red', 'green', 'blue']

        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(pv.Cone(center=(0, 0, 0)))  # red
        >>> _ = pl.add_mesh(pv.Cube(center=(1, 0, 0)))  # green
        >>> _ = pl.add_mesh(pv.Sphere(center=(1, 1, 0)))  # blue
        >>> _ = pl.add_mesh(pv.Cylinder(center=(0, 1, 0)))  # red again
        >>> pl.show()  # doctest: +SKIP

        """
        return self._color_cycler

    @color_cycler.setter
    def color_cycler(self, color_cycler):
        self._color_cycler = get_cycler(color_cycler)

    @property
    def nan_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the default NaN color.

        This color is used to plot all NaN values.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.nan_color = 'darkgray'

        """
        return self._nan_color

    @nan_color.setter
    def nan_color(self, nan_color: ColorLike):
        self._nan_color = Color(nan_color)

    @property
    def edge_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the default edge color.

        Examples
        --------
        Set the global edge color to 'blue'.

        >>> import pyvista as pv
        >>> pv.global_theme.edge_color = 'blue'

        """
        return self._edge_color

    @edge_color.setter
    def edge_color(self, edge_color: ColorLike):
        self._edge_color = Color(edge_color)

    @property
    def line_width(self) -> float:  # numpydoc ignore=RT01
        """Return or set the default line width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.line_width = 2.0

        """
        return self._line_width

    @line_width.setter
    def line_width(self, line_width: float):
        self._line_width = float(line_width)

    @property
    def point_size(self) -> float:  # numpydoc ignore=RT01
        """Return or set the default point size.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.line_width = 10.0

        """
        return self._point_size

    @point_size.setter
    def point_size(self, point_size: float):
        self._point_size = float(point_size)

    @property
    def outline_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the default outline color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.outline_color = 'white'

        """
        return self._outline_color

    @outline_color.setter
    def outline_color(self, outline_color: ColorLike):
        self._outline_color = Color(outline_color)

    @property
    def floor_color(self) -> Color:  # numpydoc ignore=RT01
        """Return or set the default floor color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.floor_color = 'black'

        """
        return self._floor_color

    @floor_color.setter
    def floor_color(self, floor_color: ColorLike):
        self._floor_color = Color(floor_color)

    @property
    def colorbar_orientation(self) -> str:  # numpydoc ignore=RT01
        """Return or set the default colorbar orientation.

        Must be either ``'vertical'`` or ``'horizontal'``.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_orientation = 'horizontal'

        """
        return self._colorbar_orientation

    @colorbar_orientation.setter
    def colorbar_orientation(self, colorbar_orientation: str):
        if colorbar_orientation not in ['vertical', 'horizontal']:
            msg = 'Colorbar orientation must be either "vertical" or "horizontal"'
            raise ValueError(msg)
        self._colorbar_orientation = colorbar_orientation

    @property
    def colorbar_horizontal(self) -> _ColorbarConfig:  # numpydoc ignore=RT01
        """Return or set the default parameters of a horizontal colorbar.

        Examples
        --------
        Set the default horizontal colorbar width to 0.6.

        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.width = 0.6

        Set the default horizontal colorbar height to 0.2.

        >>> pv.global_theme.colorbar_horizontal.height = 0.2

        """
        return self._colorbar_horizontal

    @colorbar_horizontal.setter
    def colorbar_horizontal(self, config: _ColorbarConfig):
        if not isinstance(config, _ColorbarConfig):
            msg = 'Configuration type must be `_ColorbarConfig`.'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._colorbar_horizontal = config

    @property
    def colorbar_vertical(self) -> _ColorbarConfig:  # numpydoc ignore=RT01
        """Return or set the default parameters of a vertical colorbar.

        Examples
        --------
        Set the default colorbar width to 0.45.

        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_vertical.width = 0.45

        Set the default colorbar height to 0.8.

        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_vertical.height = 0.8

        """
        return self._colorbar_vertical

    @colorbar_vertical.setter
    def colorbar_vertical(self, config: _ColorbarConfig):
        if not isinstance(config, _ColorbarConfig):
            msg = 'Configuration type must be `_ColorbarConfig`.'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._colorbar_vertical = config

    @property
    def show_scalar_bar(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default color bar visibility.

        Examples
        --------
        Show the scalar bar by default when scalars are available.

        >>> import pyvista as pv
        >>> pv.global_theme.show_scalar_bar = True

        """
        return self._show_scalar_bar

    @show_scalar_bar.setter
    def show_scalar_bar(self, show_scalar_bar: bool):
        self._show_scalar_bar = bool(show_scalar_bar)

    @property
    def show_edges(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default edge visibility.

        Examples
        --------
        Show edges globally by default.

        >>> import pyvista as pv
        >>> pv.global_theme.show_edges = True

        """
        return self._show_edges

    @show_edges.setter
    def show_edges(self, show_edges: bool):
        self._show_edges = bool(show_edges)

    @property
    def show_vertices(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default vertex visibility.

        Examples
        --------
        Show vertices globally by default.

        >>> import pyvista as pv
        >>> pv.global_theme.show_vertices = True

        """
        return self._show_vertices

    @show_vertices.setter
    def show_vertices(self, show_vertices: bool):
        self._show_vertices = bool(show_vertices)

    @property
    def lighting(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default ``lighting``.

        Examples
        --------
        Disable lighting globally.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting = False

        """
        return self._lighting

    @lighting.setter
    def lighting(self, lighting: bool):
        self._lighting = lighting

    @property
    def interactive(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default ``interactive`` parameter.

        Examples
        --------
        Make all plots non-interactive globally.

        >>> import pyvista as pv
        >>> pv.global_theme.interactive = False

        """
        return self._interactive

    @interactive.setter
    def interactive(self, interactive: bool):
        self._interactive = bool(interactive)

    @property
    def render_points_as_spheres(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default ``render_points_as_spheres`` parameter.

        Examples
        --------
        Render points as spheres by default globally.

        >>> import pyvista as pv
        >>> pv.global_theme.render_points_as_spheres = True

        """
        return self._render_points_as_spheres

    @render_points_as_spheres.setter
    def render_points_as_spheres(self, render_points_as_spheres: bool):
        self._render_points_as_spheres = bool(render_points_as_spheres)

    @property
    def point_shape(self) -> str | None:  # numpydoc ignore=RT01
        """Return or set the default point sprite shape.

        .. versionadded:: 0.48

        When set, points are rendered as the specified shape instead of
        squares. This automatically disables ``render_points_as_spheres``.

        Accepts a :class:`~pyvista.plotting.opts.PointSpriteShape` enum
        value or a string. Must be one of ``'circle'``, ``'triangle'``,
        ``'hexagon'``, ``'diamond'``, ``'asterisk'``, ``'star'``, or
        ``None``.

        Examples
        --------
        Render all points as circles by default globally.

        >>> import pyvista as pv
        >>> pv.global_theme.point_shape = 'circle'

        Or use the enum.

        >>> from pyvista.plotting.opts import PointSpriteShape
        >>> pv.global_theme.point_shape = PointSpriteShape.CIRCLE

        """
        return self._point_shape

    @point_shape.setter
    def point_shape(self, point_shape: PointSpriteShape | str | None):
        if point_shape is not None:
            try:
                point_shape = PointSpriteShape(point_shape)
            except ValueError:
                valid = ', '.join(s.value for s in PointSpriteShape)
                msg = f'Invalid point_shape {point_shape!r}. Must be one of: {valid}'
                raise ValueError(msg) from None
            point_shape = point_shape.value
        self._point_shape = point_shape

    @property
    def render_lines_as_tubes(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default ``render_lines_as_tubes`` parameter.

        Examples
        --------
        Render points as spheres by default globally.

        >>> import pyvista as pv
        >>> pv.global_theme.render_lines_as_tubes = True

        """
        return self._render_lines_as_tubes

    @render_lines_as_tubes.setter
    def render_lines_as_tubes(self, render_lines_as_tubes: bool):
        self._render_lines_as_tubes = bool(render_lines_as_tubes)

    @property
    def transparent_background(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default ``transparent_background`` parameter.

        Examples
        --------
        Set transparent_background globally to ``True``.

        >>> import pyvista as pv
        >>> pv.global_theme.transparent_background = True

        """
        return self._transparent_background

    @transparent_background.setter
    def transparent_background(self, transparent_background: bool):
        self._transparent_background = transparent_background

    @property
    def title(self) -> str:  # numpydoc ignore=RT01
        """Return or set the default ``title`` parameter.

        This is the VTK render window title.

        Examples
        --------
        Set title globally to 'plot'.

        >>> import pyvista as pv
        >>> pv.global_theme.title = 'plot'

        """
        return self._title

    @title.setter
    def title(self, title: str):
        self._title = title

    @property
    def anti_aliasing(self) -> str | None:  # numpydoc ignore=RT01
        """Enable or disable anti-aliasing.

        Should be either ``"ssaa"``, ``"msaa"``, ``"fxaa"``, or ``None``.

        Examples
        --------
        Use super-sampling anti-aliasing in the global theme.

        >>> import pyvista as pv
        >>> pv.global_theme.anti_aliasing = 'ssaa'
        >>> pv.global_theme.anti_aliasing
        'ssaa'

        Disable anti-aliasing in the global theme.

        >>> import pyvista as pv
        >>> pv.global_theme.anti_aliasing = None

        See :ref:`anti_aliasing_example` for more information regarding
        anti-aliasing.

        """
        return self._anti_aliasing

    @anti_aliasing.setter
    def anti_aliasing(self, anti_aliasing: str | None):
        if isinstance(anti_aliasing, str):
            if anti_aliasing not in ['ssaa', 'msaa', 'fxaa']:
                msg = 'anti_aliasing must be either "ssaa", "msaa", or "fxaa"'
                raise ValueError(msg)
        elif anti_aliasing is not None:
            msg = 'anti_aliasing must be either "ssaa", "msaa", "fxaa", or None'  # type: ignore[unreachable]
            raise TypeError(msg)

        self._anti_aliasing = anti_aliasing  # type: ignore[assignment]

    @property
    def multi_samples(self) -> int:  # numpydoc ignore=RT01
        """Return or set the default ``multi_samples`` parameter.

        Set the number of multisamples to used with hardware anti_aliasing. This
        is only used when :attr:`anti_aliasing <Theme.anti_aliasing>` is
        set to ``"msaa"``.

        Examples
        --------
        Set the default number of multisamples to 2 and enable ``"msaa"``

        >>> import pyvista as pv
        >>> pv.global_theme.anti_aliasing = 'msaa'
        >>> pv.global_theme.multi_samples = 2

        """
        return self._multi_samples

    @multi_samples.setter
    def multi_samples(self, multi_samples: int):
        self._multi_samples = int(multi_samples)

    @property
    def multi_rendering_splitting_position(self) -> float:  # numpydoc ignore=RT01
        """Return or set the default ``multi_rendering_splitting_position`` parameter.

        Examples
        --------
        Set multi_rendering_splitting_position globally to 0.5 (the
        middle of the window).

        >>> import pyvista as pv
        >>> pv.global_theme.multi_rendering_splitting_position = 0.5

        """
        return self._multi_rendering_splitting_position  # type: ignore[return-value]

    @multi_rendering_splitting_position.setter
    def multi_rendering_splitting_position(
        self,
        multi_rendering_splitting_position: float,
    ):
        self._multi_rendering_splitting_position = multi_rendering_splitting_position

    @property
    def volume_mapper(self) -> str:  # numpydoc ignore=RT01
        """Return or set the default ``volume_mapper`` parameter.

        Must be one of the following strings, which are mapped to the
        following VTK volume mappers.

        * ``'fixed_point'`` : :vtk:`vtkFixedPointVolumeRayCastMapper`
        * ``'gpu'`` : :vtk:`vtkGPUVolumeRayCastMapper`
        * ``'open_gl'`` : :vtk:`vtkOpenGLGPUVolumeRayCastMapper`
        * ``'smart'`` : :vtk:`vtkSmartVolumeMapper`

        Examples
        --------
        Set default volume mapper globally to 'gpu'.

        >>> import pyvista as pv
        >>> pv.global_theme.volume_mapper = 'gpu'

        """
        return self._volume_mapper

    @volume_mapper.setter
    def volume_mapper(self, mapper: str):
        mappers = ['fixed_point', 'gpu', 'open_gl', 'smart']
        if mapper not in mappers:
            msg = (
                f'Mapper ({mapper}) unknown. Available volume mappers '
                f'include:\n {", ".join(mappers)}'
            )
            raise ValueError(msg)

        self._volume_mapper = mapper

    @property
    def smooth_shading(self) -> bool:  # numpydoc ignore=RT01
        """Return or set the default ``smooth_shading`` parameter.

        Examples
        --------
        Set the global smooth_shading parameter default to ``True``.

        >>> import pyvista as pv
        >>> pv.global_theme.smooth_shading = True

        """
        return self._smooth_shading

    @smooth_shading.setter
    def smooth_shading(self, smooth_shading: bool):
        self._smooth_shading = bool(smooth_shading)

    @property
    def depth_peeling(self) -> _DepthPeelingConfig:  # numpydoc ignore=RT01
        """Return or set the default depth peeling parameters.

        Examples
        --------
        Set the global depth_peeling parameter default to be enabled
        with 8 peels.

        >>> import pyvista as pv
        >>> pv.global_theme.depth_peeling.number_of_peels = 8
        >>> pv.global_theme.depth_peeling.occlusion_ratio = 0.0
        >>> pv.global_theme.depth_peeling.enabled = True

        """
        return self._depth_peeling

    @depth_peeling.setter
    def depth_peeling(self, config: _DepthPeelingConfig):
        if not isinstance(config, _DepthPeelingConfig):
            msg = 'Configuration type must be `_DepthPeelingConfig`.'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._depth_peeling = config

    @property
    def silhouette(self) -> _SilhouetteConfig:  # numpydoc ignore=RT01
        """Return or set the default ``silhouette`` configuration.

        Examples
        --------
        Set parameters of the silhouette.

        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.color = 'grey'
        >>> pv.global_theme.silhouette.line_width = 2.0
        >>> pv.global_theme.silhouette.feature_angle = 20

        """
        return self._silhouette

    @silhouette.setter
    def silhouette(self, config: _SilhouetteConfig):
        if not isinstance(config, _SilhouetteConfig):
            msg = 'Configuration type must be `_SilhouetteConfig`'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._silhouette = config

    @property
    def slider_styles(self) -> _SliderConfig:  # numpydoc ignore=RT01
        """Return the default slider style configurations."""
        return self._slider_styles

    @slider_styles.setter
    def slider_styles(self, config: _SliderConfig):
        if not isinstance(config, _SliderConfig):
            msg = 'Configuration type must be `_SliderConfig`.'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._slider_styles = config

    @property
    def axes(self) -> _AxesConfig:  # numpydoc ignore=RT01
        """Return or set the default ``axes`` configuration.

        Examples
        --------
        Set the x-axis color to black.

        >>> import pyvista as pv
        >>> pv.global_theme.axes.x_color = 'black'

        Show the axes orientation widget by default.

        >>> pv.global_theme.axes.show = True

        Use the :func:`axes orientation box <pyvista.create_axes_orientation_box>`
        as the orientation widget.

        >>> pv.global_theme.axes.box = True

        """
        return self._axes

    @axes.setter
    def axes(self, config: _AxesConfig):
        if not isinstance(config, _AxesConfig):
            msg = 'Configuration type must be `_AxesConfig`.'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._axes = config

    @property
    def before_close_callback(
        self,
    ) -> Callable[[pyvista.Plotter], None]:  # numpydoc ignore=RT01
        """Return the default before_close_callback function for Plotter."""
        return self._before_close_callback  # type: ignore[return-value]

    @before_close_callback.setter
    def before_close_callback(
        self,
        value: Callable[[pyvista.Plotter], None],
    ):
        self._before_close_callback = value

    @property
    def allow_empty_mesh(self) -> bool:  # numpydoc ignore=RT01
        """Return or set whether to allow plotting empty meshes.

        Examples
        --------
        Enable plotting of empty meshes.

        >>> import pyvista as pv
        >>> pv.global_theme.allow_empty_mesh = True

        Now add an empty mesh to a plotter

        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(pv.PolyData())
        >>> pl.show()  # doctest: +SKIP

        """
        return self._allow_empty_mesh

    @allow_empty_mesh.setter
    def allow_empty_mesh(self, allow_empty_mesh: bool):
        self._allow_empty_mesh = bool(allow_empty_mesh)

    def restore_defaults(self):
        """Restore the theme defaults.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.restore_defaults()

        """
        self.__init__()  # type: ignore[misc]

    def __repr__(self):
        """User friendly representation of the current theme."""
        txt = [f'{self.name.capitalize()} Theme']
        txt.append('-' * len(txt[0]))
        parm = {
            'Background': 'background',
            'Jupyter backend': 'jupyter_backend',
            'Full screen': 'full_screen',
            'Window size': 'window_size',
            'Camera': 'camera',
            'Notebook': 'notebook',
            'Font': 'font',
            'Auto close': 'auto_close',
            'Colormap': 'cmap',
            'Color': 'color',
            'Color Cycler': 'color_cycler',
            'NaN color': 'nan_color',
            'Edge color': 'edge_color',
            'Outline color': 'outline_color',
            'Floor color': 'floor_color',
            'Colorbar orientation': 'colorbar_orientation',
            'Colorbar - horizontal': 'colorbar_horizontal',
            'Colorbar - vertical': 'colorbar_vertical',
            'Show scalar bar': 'show_scalar_bar',
            'Show edges': 'show_edges',
            'Lighting': 'lighting',
            'Interactive': 'interactive',
            'Interactor style': 'interactor_style',
            'Render points as spheres': 'render_points_as_spheres',
            'Transparent Background': 'transparent_background',
            'Title': 'title',
            'Axes': 'axes',
            'Multi-samples': 'multi_samples',
            'Multi-renderer Split Pos': 'multi_rendering_splitting_position',
            'Volume mapper': 'volume_mapper',
            'Smooth shading': 'smooth_shading',
            'Depth peeling': 'depth_peeling',
            'Silhouette': 'silhouette',
            'Slider Styles': 'slider_styles',
            'Return Camera Position': 'return_cpos',
            'Hidden Line Removal': 'hidden_line_removal',
            'Anti-Aliasing': '_anti_aliasing',
            'Split sharp edges': '_split_sharp_edges',
            'Sharp edges feat. angle': '_sharp_edges_feature_angle',
            'Before close callback': '_before_close_callback',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'{name:<25}: {setting}')

        return '\n'.join(txt)

    @property
    def name(self) -> str:  # numpydoc ignore=RT01
        """Return or set the name of the theme."""
        return self._name

    @name.setter
    def name(self, name: str):
        self._name = name

    @property
    def interactor_style(self) -> str:  # numpydoc ignore=RT01
        """Return or set the default interactor style.

        The value must be the name of a built-in or registered
        interactor style. Built-in styles use names that mirror the
        public ``enable_*_style`` methods, such as
        ``'terrain_style'``.

        Returns
        -------
        str
            The default interactor style name.

        Examples
        --------
        Set the default interactor style to terrain.

        >>> import pyvista as pv
        >>> pv.global_theme.interactor_style = 'terrain_style'

        Register and use a custom interactor style.

        >>> def custom_style(interactor): ...
        >>> pv.register_interactor_style(
        ...     'custom_style', custom_style
        ... )  # doctest: +SKIP
        >>> pv.global_theme.interactor_style = 'custom_style'  # doctest: +SKIP

        """
        return self._interactor_style

    @interactor_style.setter
    def interactor_style(self, interactor_style: str) -> None:
        """Set the default interactor style."""
        self._interactor_style = _validate_interactor_style(interactor_style)

    def load_theme(self, theme: str | Theme) -> None:
        """Overwrite the current theme with a theme.

        Parameters
        ----------
        theme : pyvista.plotting.themes.Theme
            Theme to use to overwrite this theme.

        Examples
        --------
        Create a custom theme from the default theme and load it into
        the global theme of pyvista.

        >>> import pyvista as pv
        >>> from pyvista.plotting.themes import DocumentTheme
        >>> my_theme = DocumentTheme()
        >>> my_theme.font.size = 20
        >>> my_theme.font.title_size = 40
        >>> my_theme.cmap = 'jet'
        >>> pv.global_theme.load_theme(my_theme)
        >>> pv.global_theme.font.size
        20

        Create a custom theme from the dark theme and load it into
        pyvista.

        >>> from pyvista.plotting.themes import DarkTheme
        >>> my_theme = DarkTheme()
        >>> my_theme.show_edges = True
        >>> pv.global_theme.load_theme(my_theme)
        >>> pv.global_theme.show_edges
        True

        """
        if isinstance(theme, str):
            theme = load_theme(theme)

        if not isinstance(theme, Theme):
            msg = '``theme`` must be a pyvista theme like ``pyvista.plotting.themes.Theme``.'
            raise TypeError(msg)

        for attr_name in Theme.__slots__:
            setattr(self, attr_name, getattr(theme, attr_name))

    def save(self, filename: str) -> None:
        """Serialize this theme to a json file.

        ``before_close_callback`` is non-serializable and is omitted.

        Parameters
        ----------
        filename : str
            Path to save the theme to.  Should end in ``'.json'``.

        Examples
        --------
        Export and then load back in a theme.

        >>> import pyvista as pv
        >>> theme = pv.themes.DocumentTheme()
        >>> theme.background = 'white'
        >>> theme.save('my_theme.json')  # doctest:+SKIP
        >>> loaded_theme = pv.load_theme('my_theme.json')  # doctest:+SKIP

        """
        data = self.to_dict()
        # functions are not serializable
        del data['before_close_callback']
        with Path(filename).open('w') as f:
            json.dump(data, f)

    @property
    def split_sharp_edges(self) -> bool:  # numpydoc ignore=RT01
        """Set or return splitting sharp edges.

        See :ref:`shading_example` for an example showing split sharp edges.

        Examples
        --------
        Enable the splitting of sharp edges globally.

        >>> import pyvista as pv
        >>> pv.global_theme.split_sharp_edges = True
        >>> pv.global_theme.split_sharp_edges
        True

        Disable the splitting of sharp edges globally.

        >>> import pyvista as pv
        >>> pv.global_theme.split_sharp_edges = False
        >>> pv.global_theme.split_sharp_edges
        False

        """
        return self._split_sharp_edges

    @split_sharp_edges.setter
    def split_sharp_edges(self, value: bool):
        self._split_sharp_edges = value

    @property
    def sharp_edges_feature_angle(self) -> float:  # numpydoc ignore=RT01
        """Set or return the angle of the sharp edges feature angle.

        See :ref:`shading_example` for an example showing split sharp edges.

        Examples
        --------
        Change the sharp edges feature angle to 45 degrees.

        >>> import pyvista as pv
        >>> pv.global_theme.sharp_edges_feature_angle = 45.0
        >>> pv.global_theme.sharp_edges_feature_angle
        45.0

        """
        return self._sharp_edges_feature_angle

    @sharp_edges_feature_angle.setter
    def sharp_edges_feature_angle(self, value: float):
        self._sharp_edges_feature_angle = float(value)

    @property
    def lighting_params(self) -> _LightingConfig:  # numpydoc ignore=RT01
        """Return or set the default lighting configuration."""
        return self._lighting_params

    @lighting_params.setter
    def lighting_params(self, config: _LightingConfig):
        if not isinstance(config, _LightingConfig):
            msg = 'Configuration type must be `_LightingConfig`.'  # type: ignore[unreachable]
            raise TypeError(msg)
        self._lighting_params = config

    @property
    def resample_environment_texture(self) -> bool | float:  # numpydoc ignore=RT01
        """Set or return resampling environment texture.

        Resample the environment texture when using
        :meth:`~pyvista.Plotter.set_environment_texture`.
        Set this to a float to set the sampling rate explicitly or set
        to ``True`` to downsample the texture to 1/16th of its original
        resolution.

        Downsampling the texture can substantially improve performance for
        some environments, e.g. headless setups or if GPU support is limited.

        .. versionadded:: 0.45

        Examples
        --------
        Enable resampling the environment texture globally.

        >>> import pyvista as pv
        >>> pv.global_theme.resample_environment_texture = True
        >>> pv.global_theme.resample_environment_texture
        True

        Disable the resampling the environment texture.

        >>> import pyvista as pv
        >>> pv.global_theme.resample_environment_texture = False
        >>> pv.global_theme.resample_environment_texture
        False

        """
        return self._resample_environment_texture

    @resample_environment_texture.setter
    def resample_environment_texture(self, value: bool | float):
        self._resample_environment_texture = value

    @property
    def logo_file(self) -> str | None:  # numpydoc ignore=RT01
        """Return or set the logo file.

        .. note::

            :func:`pyvista.Plotter.add_logo_widget` will default to
            PyVista's logo if this is unset.

        Examples
        --------
        Set the logo file to a custom logo.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> logo_file = examples.download_file('vtk.png')
        >>> pv.global_theme.logo_file = logo_file

        Now the logo will be used by default for :func:`pyvista.Plotter.add_logo_widget`.

        >>> pl = pv.Plotter()
        >>> _ = pl.add_logo_widget()
        >>> _ = pl.add_mesh(pv.Sphere(), show_edges=True)
        >>> pl.show()

        """
        return self._logo_file

    @logo_file.setter
    def logo_file(self, logo_file: str | pathlib.Path | None):
        if logo_file is None:
            path = None
        else:
            if not pathlib.Path(logo_file).exists():
                msg = f'Logo file ({logo_file}) not found.'
                raise FileNotFoundError(msg)
            path = str(logo_file)
        self._logo_file = path


class DarkTheme(Theme):
    """Dark mode theme.

    Black background, "viridis" colormap, tan meshes, white (hidden) edges.

    Examples
    --------
    Make the dark theme the global default.

    >>> import pyvista as pv
    >>> from pyvista import themes
    >>> pv.set_plot_theme(themes.DarkTheme())

    Alternatively, set via a string.

    >>> pv.set_plot_theme('dark')

    """

    _default_name: ClassVar[str] = 'dark'

    def __init__(self):
        """Initialize the theme."""
        super().__init__()
        self.background = 'black'
        self.cmap = 'viridis'
        self.font.color = 'white'
        self.show_edges = False
        self.color = 'lightblue'
        self.outline_color = 'white'
        self.edge_color = 'white'
        self.axes.x_color = 'tomato'
        self.axes.y_color = 'seagreen'
        self.axes.z_color = 'blue'


class ParaViewTheme(Theme):
    """A paraview-like theme.

    Examples
    --------
    Make the paraview-like theme the global default.

    >>> import pyvista as pv
    >>> from pyvista import themes
    >>> pv.set_plot_theme(themes.ParaViewTheme())

    Alternatively, set via a string.

    >>> pv.set_plot_theme('paraview')

    """

    _default_name: ClassVar[str] = 'paraview'

    def __init__(self):
        """Initialize theme."""
        super().__init__()
        self.background = 'paraview'
        self.cmap = 'coolwarm'
        self.font.family = 'arial'
        self.font.label_size = 16
        self.font.color = 'white'
        self.show_edges = False
        self.color = 'white'
        self.outline_color = 'white'
        self.edge_color = 'black'
        self.axes.x_color = 'tomato'
        self.axes.y_color = 'gold'
        self.axes.z_color = 'green'


class DocumentTheme(Theme):
    """A document theme well suited for papers and presentations.

    This theme uses:

    * A white background
    * Black fonts
    * The "viridis" colormap
    * disables edges for surface plots
    * Hidden edge removal

    Best used for presentations, papers, etc.

    Examples
    --------
    Make the document theme the global default.

    >>> import pyvista as pv
    >>> from pyvista import themes
    >>> pv.set_plot_theme(themes.DocumentTheme())

    Alternatively, set via a string.

    >>> pv.set_plot_theme('document')

    """

    _default_name: ClassVar[str] = 'document'

    def __init__(self):
        """Initialize the theme."""
        super().__init__()
        self.background = 'white'
        self.cmap = 'viridis'
        self.font.size = 18
        self.font.title_size = 18
        self.font.label_size = 18
        self.font.color = 'black'
        self.show_edges = False
        self.color = 'lightblue'
        self.outline_color = 'black'
        self.edge_color = 'black'
        self.axes.x_color = 'tomato'
        self.axes.y_color = 'seagreen'
        self.axes.z_color = 'blue'


class DocumentProTheme(DocumentTheme):
    """A more professional document theme.

    This theme extends the base document theme with:

    * Default color cycling
    * Rendering points as spheres
    * MSAA anti aliassing
    * Depth peeling

    """

    _default_name: ClassVar[str] = 'document_pro'

    def __init__(self):
        """Initialize the theme."""
        super().__init__()
        self.anti_aliasing = 'ssaa'
        self.color_cycler = get_cycler('default')
        self.render_points_as_spheres = True
        self.multi_samples = 8
        self.depth_peeling.number_of_peels = 4
        self.depth_peeling.occlusion_ratio = 0.0
        self.depth_peeling.enabled = True


class _DocumentBuildTheme(DocumentTheme):
    """Theme used for building the documentation."""

    _default_name: ClassVar[str] = 'document_build'

    def __init__(self):
        """Initialize the theme."""
        super().__init__()
        self.window_size = [1024, 768]
        self.font.size = 22
        self.font.label_size = 22
        self.font.title_size = 22
        self.return_cpos = False
        self.resample_environment_texture = True

        self._plot_cell._line_width = 10
        self._plot_cell._point_size = 80
        self._plot_cell._font_size = 50
        self._plot_cell._normals_scale = 0.25


class _TestingTheme(Theme):
    """Low resolution testing theme for ``pytest``.

    Necessary for image regression.  Xvfb doesn't support
    multi-sampling, it's disabled for consistency between desktops and
    remote testing.

    Also disables ``return_cpos`` to make it easier for us to write
    examples without returning camera positions.

    Resampling is also enabled for environment textures since this
    can be very slow without a GPU.

    """

    _default_name: ClassVar[str] = 'testing'

    def __init__(self):
        super().__init__()
        self.multi_samples = 1
        self.window_size = [400, 400]
        self.axes.show = False
        self.return_cpos = False
        self.resample_environment_texture = True

        self._plot_cell._line_width = 8
        self._plot_cell._point_size = 50
        self._plot_cell._font_size = 30
        self._plot_cell._normals_scale = 0.2


class _NATIVE_THEMES(Enum):  # noqa: N801
    """Global built-in themes available to PyVista."""

    paraview = ParaViewTheme
    document = DocumentTheme
    document_pro = DocumentProTheme
    document_build = _DocumentBuildTheme
    dark = DarkTheme
    default = document
    testing = _TestingTheme
    vtk = Theme


# Register legacy name aliases. ``DocumentTheme`` already self-registers as
# ``'document'`` via ``__init_subclass__``; these aliases preserve the
# historical ``'default' -> DocumentTheme`` and ``'vtk' -> Theme`` mappings
# that ``_NATIVE_THEMES`` provided.
_register_alias('default', DocumentTheme)
_register_alias('vtk', Theme)
