'''
ScrollView
==========

.. versionadded:: 1.0.4

The :class:`ScrollView` widget provides a scrollable/pannable viewport that is
clipped at the scrollview's bounding box.

.. note::
    Use :class:`~kivy.uix.recycleview.RecycleView` for generating large
    numbers of widgets in order to display many data items.


Scrolling Behavior
------------------

The ScrollView accepts only one child and applies a viewport/window to
it according to the :attr:`~ScrollView.scroll_x` and
:attr:`~ScrollView.scroll_y` properties. Touches are analyzed to
determine if the user wants to scroll or control the child in some
other manner: you cannot do both at the same time. To determine if
interaction is a scrolling gesture, these properties are used:

    - :attr:`~ScrollView.scroll_distance`: the minimum distance to travel,
      defaults to 20 pixels.
    - :attr:`~ScrollView.scroll_timeout`: the maximum time period, defaults
      to 55 milliseconds.

If a touch travels :attr:`~ScrollView.scroll_distance` pixels within the
:attr:`~ScrollView.scroll_timeout` period, it is recognized as a scrolling
gesture and translation (scroll/pan) will begin. If the timeout occurs, the
touch down event is dispatched to the child instead (no translation).

The default value for those settings can be changed in the configuration file::

    [widgets]
    scroll_timeout = 250
    scroll_distance = 20

.. versionadded:: 1.1.1

    ScrollView now animates scrolling in Y when a mousewheel is used.


Limiting to the X or Y Axis
---------------------------

By default, the ScrollView allows scrolling along both the X and Y axes. You
can explicitly disable scrolling on an axis by setting the
:attr:`~ScrollView.do_scroll_x` or :attr:`~ScrollView.do_scroll_y` properties
to False.


Managing the Content Size and Position
--------------------------------------

The ScrollView manages the position of its children similarly to a
:class:`~kivy.uix.relativelayout.RelativeLayout` but does not use the
:attr:`~kivy.uix.widget.Widget.size_hint`. You must
carefully specify the :attr:`~kivy.uix.widget.Widget.size` of your content to
get the desired scroll/pan effect.

By default, the :attr:`~kivy.uix.widget.Widget.size_hint` is (1, 1), so the
content size will fit your ScrollView
exactly (you will have nothing to scroll). You must deactivate at least one of
the size_hint instructions (x or y) of the child to enable scrolling.
Setting :attr:`~kivy.uix.widget.Widget.size_hint_min` to not be None will
also enable scrolling for that dimension when the :class:`ScrollView` is
smaller than the minimum size.

To scroll a :class:`~kivy.uix.gridlayout.GridLayout` on it's Y-axis/vertically,
set the child's width  to that of the ScrollView (size_hint_x=1), and set
the size_hint_y property to None::

    from kivy.uix.gridlayout import GridLayout
    from kivy.uix.button import Button
    from kivy.uix.scrollview import ScrollView
    from kivy.core.window import Window
    from kivy.app import runTouchApp

    layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
    # Make sure the height is such that there is something to scroll.
    layout.bind(minimum_height=layout.setter('height'))
    for i in range(100):
        btn = Button(text=str(i), size_hint_y=None, height=40)
        layout.add_widget(btn)
    root = ScrollView(size_hint=(1, None), size=(Window.width, Window.height))
    root.add_widget(layout)

    runTouchApp(root)


Kv Example::

    ScrollView:
        do_scroll_x: False
        do_scroll_y: True

        Label:
            size_hint_y: None
            height: self.texture_size[1]
            text_size: self.width, None
            padding: 10, 10
            text:
                'really some amazing text\\n' * 100

Overscroll Effects
------------------

.. versionadded:: 1.7.0

When scrolling would exceed the bounds of the :class:`ScrollView`, it
uses a :class:`~kivy.effects.scroll.ScrollEffect` to handle the
overscroll. These effects can perform actions like bouncing back,
changing opacity, or simply preventing scrolling beyond the normal
boundaries. Note that complex effects may perform many computations,
which can be slow on weaker hardware.

You can change what effect is being used by setting
:attr:`~ScrollView.effect_cls` to any effect class. Current options
include:

    - :class:`~kivy.effects.scroll.ScrollEffect`: Does not allow
      scrolling beyond the :class:`ScrollView` boundaries.
    - :class:`~kivy.effects.dampedscroll.DampedScrollEffect`: The
      current default. Allows the user to scroll beyond the normal
      boundaries, but has the content spring back once the
      touch/click is released.
    - :class:`~kivy.effects.opacityscroll.OpacityScrollEffect`: Similar
      to the :class:`~kivy.effect.dampedscroll.DampedScrollEffect`, but
      also reduces opacity during overscroll.

You can also create your own scroll effect by subclassing one of these,
then pass it as the :attr:`~ScrollView.effect_cls` in the same way.

Alternatively, you can set :attr:`~ScrollView.effect_x` and/or
:attr:`~ScrollView.effect_y` to an *instance* of the effect you want to
use. This will override the default effect set in
:attr:`~ScrollView.effect_cls`.

All the effects are located in the :mod:`kivy.effects`.

Nested ScrollViews
------------------
An application can have multiple levels of nested ScrollViews.  The
ScrollView class manages the hierarchy of nested ScrollViews.

It is important to set the :attr:`do_scroll_x` and :attr:`do_scroll_y`
properties to the correct values for the nested ScrollViews.
The nested scrolling behavior, described below, is only applied if the
:attr:`do_scroll_x` and :attr:`do_scroll_y` properties are set to the
appropriate values.

To achieve orthogonal scrolling, for example, the outer
ScrollView should have :attr:`do_scroll_x` set to False and the inner
ScrollView should have :attr:`do_scroll_y` set to True, or vice versa.

For Parallel scrolling, both ScrollViews should have the same value for
:attr:`do_scroll_x` and :attr:`do_scroll_y`.  This can allow vertical in
vertical,  horizontal in horizontal, or XY in XY scrolling.

For mixed scrolling, the outer ScrollView should have :attr:`do_scroll_x`
and :attr:`do_scroll_y` set to True, and the inner ScrollView should have
either :attr:`do_scroll_x` or :attr:`do_scroll_y` set to False.


**Orthogonal Scrolling** (outer and inner scroll in different directions):
    - Touch: Each ScrollView scrolls only in its direction
    - Wheel: Innermost ScrollView scrolls if it supports that axis
    - Example: Vertical outer + Horizontal inner

**Parallel Scrolling** (outer and inner scroll in the same direction):
    - Touch: Web-style boundary delegation (see below)
    - Wheel: Innermost ScrollView scrolls, no delegation to outer
    - Scrollbar: Scroll does not affect other ScrollView
    - Example: Vertical outer + Vertical inner

**Mixed Scrolling** (outer scrolls XY, inner one axis, or vice versa):
    - Shared axis: Web-style boundary delegation
    - Exclusive axes: Immediate delegation or inner-only scrolling
    - Wheel: Innermost ScrollView scrolls if it supports the axis
    - Example: XY outer + Horizontal inner


Web-Style Boundary Delegation
----------------------------

For parallel and shared-axis scrolling, ScrollView uses web-style
delegation:

    - If a touch starts at the inner boundary and moves out, it delegates
      scrolling to the outer ScrollView right away.
    - If a touch starts at the inner boundary and moves in, only the inner
      ScrollView scrolls.
    - If a touch starts not at a boundary, only the inner ScrollView
      scrolls, even if the gesture later reaches a boundary. Delegation to
      the outer never occurs mid-gesture in this case.
    - A new touch starting at the boundary is required to delegate to the
      outer.

Disable this behavior by setting :attr:`parallel_delegation` to False.

Wheel Behavior in Nested ScrollViews
------------------------------------

When the mouse scroll wheel or a trackpad is used, ScrollView always
applies web-style delegation:

  - Only the innermost ScrollView under the pointer/cursor will handle
    the wheel event.
  - If that ScrollView cannot scroll further along the wheel axis, the
    event is not propagated to outer ScrollViews. This matches standard
    browser and OS behavior.
  - Outer ScrollViews never respond to wheel events unless the pointer is
    over their scrollbars or unless there are no deeper nested
    ScrollViews.

This prevents "scroll hijacking" and provides an intuitive nested scroll
experience.

**Examples:**
  - In a vertical ScrollView containing a horizontal ScrollView, using the
    wheel over the horizontal ScrollView means only the vertical (outer)
    ScrollView scrolls, if its direction matches the wheel.
  - For two nested vertical ScrollViews, the innermost one under the
    pointer will scroll with the wheel until it cannot scroll further;
    wheel events are not delegated to the parent.

Wheel behavior is always active and is NOT affected by the
:attr:`parallel_delegation` or :attr:`delegate_to_outer` properties.
Those only control touch and touchpad gesture behavior.

.. versionchanged:: VERSION_NEXT

    The ScrollView widget now supports nesting to arbitrary levels
    and configurations.
'''

__all__ = ("ScrollView",)

from functools import partial
from math import isclose
from enum import Enum

from kivy.animation import Animation
from kivy.config import Config
from kivy.clock import Clock
from kivy.factory import Factory
from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas
from kivy.uix.stencilview import StencilView
from kivy.metrics import dp
from kivy.effects.dampedscroll import DampedScrollEffect
from kivy.properties import (
    NumericProperty,
    BooleanProperty,
    AliasProperty,
    ObjectProperty,
    ListProperty,
    ReferenceListProperty,
    OptionProperty,
    ColorProperty,
)
from kivy.uix.behaviors import FocusBehavior


# =============================================================================
# STATE MACHINE ENUMS
# =============================================================================


class ScrollMode(str, Enum):
    # Touch intent detection state machine.

    # Tracks whether a touch gesture is a tap/click or a scroll gesture
    # based on movement distance and timeout thresholds.

    # State Transition Diagram:
    #
    #                          movement > scroll_distance
    #                    ┌──────────────────────────────────┐
    #                    │                                  │
    #                    │                                  v
    #              ┌─────────┐                        ┌────────┐
    #              │ UNKNOWN │                        │ SCROLL │
    #              └─────────┘                        └────────┘
    #                    │                                 ^
    #                    │ timeout expires                 │
    #                    v                                 │
    #         try _simulate_touch_down()                   │
    #                    │                                 │
    #         ┌──────────┴──────────┐                      │
    #         │                     │                      │
    #    child grabbed         no child grabbed            │
    #         │                     │                      │
    #         v                     └──────────────────────┘
    #   hand off to child         transition to SCROLL mode
    #   (button, etc.)            (label, empty space, etc.)
    #
    # State Descriptions:
    # - UNKNOWN: Initial state, accumulating movement to detect intent
    # - SCROLL: Scroll gesture confirmed - either movement exceeded threshold
    #           OR timeout expired without child widget consuming the touch
    UNKNOWN = "unknown"  # Detecting intent - accumulating movement
    SCROLL = "scroll"  # Confirmed scroll gesture - movement exceeded threshold


class DelegationMode(str, Enum):
    # Web-style boundary delegation state machine.

    # Controls when an inner ScrollView delegates scrolling to its outer
    # ScrollView in parallel nested configurations (both scrolling same axis).

    # State Descriptions:
    # - UNLOCKED: Normal scrolling, no delegation. Inner scrolls freely.
    # - START_AT_BOUNDARY: Touch began at inner's boundary.
    #                      Watching movement direction.
    # - LOCKED: Touch trying to scroll beyond boundary.
    #           Outer takes over, inner blocked.

    # State Transition Diagram:

    # Touch starts NOT at boundary:
    #     ┌──────────┐
    #     │ UNLOCKED │ (stays UNLOCKED entire gesture, inner scrolls freely)
    #     └──────────┘

    # Touch starts AT boundary:
    #     ┌──────────────────┐
    #     │ START_AT_BOUNDARY│─────┐
    #     └──────────────────┘     │
    #             │                │
    #             │                │
    #     move into content  scroll beyond boundary
    #             │                │
    #             v                v
    #     ┌──────────┐         ┌────────┐
    #     │ UNLOCKED │         │ LOCKED │ (stays LOCKED entire gesture)
    #     └──────────┘         └────────┘

    # Key: Once LOCKED, stays LOCKED until touch release (no transitions out)

    # Example (Vertical Parallel):
    # 1. User touches inner ScrollView at bottom (scroll_y = 1.0)
    #    -> START_AT_BOUNDARY
    # 2a. User drags down (into content) -> UNLOCKED (inner scrolls normally)
    # 2b. User drags up (beyond bottom edge) -> LOCKED (outer takes over)
    # 3. If LOCKED: stays LOCKED, outer handles all further movement

    UNLOCKED = "unlocked"  # Normal scrolling, no delegation active
    START_AT_BOUNDARY = "start_at_boundary"  # Touch began at scroll boundary
    LOCKED = "locked"  # At boundary trying to scroll beyond - delegate to outer


# =============================================================================
# HIERARCHY MANAGEMENT CLASS
# =============================================================================


class ScrollViewHierarchy:
    # INTERNAL CLASS - Manages the hierarchy of nested ScrollViews for a
    # touch gesture.
    #
    # Stores ScrollViews from outermost (index 0) to innermost (index n).
    # Each ScrollView can use its index to find its parent and navigate
    # the chain.
    #
    # Design Philosophy:
    # - ScrollViews are stored in a list
    #   (scrollviews[0] = outer, scrollviews[-1] = inner)
    # - Relationship data (classification, axis_config) stored separately
    # - Classifications describe relationships BETWEEN levels,
    #   not properties OF levels
    # - Index arithmetic is encapsulated in methods for safety
    #
    # Example for 3-level hierarchy (outer → middle → inner):
    #     scrollviews = [outer, middle, inner]
    #     _parent_child_data = [
    #         {classification: 'parallel', axis_config: None},  # outer→middle
    #         {classification: 'orthogonal', axis_config: None} # middle→inner
    #     ]

    def __init__(self, outer_sv):
        # Initialize hierarchy with the outermost ScrollView.
        # Args:
        #   outer_sv: The outermost ScrollView in the hierarchy
        self.scrollviews = [outer_sv]  # List from outer to inner
        self._parent_child_data = (
            []
        )  # Relationship data between adjacent levels
        self.touched_index = 0  # Index of ScrollView where touch started

    def add_child(self, child_sv, classification, axis_config=None):
        # Add a child ScrollView to the hierarchy.
        # Args:
        #   child_sv: The child ScrollView to add
        #   classification: 'orthogonal', 'parallel', or 'mixed'
        #   axis_config: Dict with axis configuration (only for mixed)
        self.scrollviews.append(child_sv)
        self._parent_child_data.append(
            {"classification": classification, "axis_config": axis_config}
        )
        self.touched_index = len(self.scrollviews) - 1

    def get_parent(self, current_index):
        # Get parent ScrollView for given index.
        # Args:
        #   current_index: Index of the child ScrollView
        # Returns:
        #   Parent ScrollView, or None if at root (index 0)
        if current_index > 0:
            return self.scrollviews[current_index - 1]
        return None

    def get_relationship(self, child_index):
        # Get relationship data between child at child_index and its parent.
        # Encapsulates the index arithmetic for accessing relationship data.
        # Args:
        #   child_index: Index of the child ScrollView
        # Returns:
        #   Dict with 'classification' and 'axis_config', or None if no parent
        if 0 < child_index < len(self.scrollviews):
            return self._parent_child_data[child_index - 1]
        return None

    def get_classification(self, child_index):
        # Get classification between child and its parent.
        # Args:
        #   child_index: Index of the child ScrollView
        # Returns:
        #   Classification string ('orthogonal', 'parallel', 'mixed'),
        #   or None if child_index is 0 (no parent)
        rel = self.get_relationship(child_index)
        return rel["classification"] if rel else None

    def get_axis_config(self, child_index):
        # Get axis configuration for mixed classification.
        # Args:
        #   child_index: Index of the child ScrollView
        # Returns:
        #   Axis config dict for mixed cases, or None
        rel = self.get_relationship(child_index)
        return rel["axis_config"] if rel else None

    @property
    def depth(self):
        # Total depth of hierarchy (number of levels).
        return len(self.scrollviews)

    @property
    def outer(self):
        # Outermost ScrollView (index 0).
        return self.scrollviews[0]

    @property
    def inner(self):
        # Innermost ScrollView (last in list).
        return self.scrollviews[-1]


# =============================================================================
# CONFIGURATION CONSTANTS
# =============================================================================

# Boundary threshold for web-style delegation
# (normalized scroll position 0.0 to 1.0)
# A ScrollView is considered "at boundary" if scroll position is
# within this threshold of the minimum (0.0) or maximum (1.0) position.
# Used for parallel nested scrolling to determine when to delegate
# to outer ScrollView.
# Note: Set to 15% to account for elastic overscroll bounce-back
_BOUNDARY_THRESHOLD = 0.05  # 5% from edge

# When we are generating documentation, Config doesn't exist
_scroll_timeout = _scroll_distance = 0
if Config:
    _scroll_timeout = Config.getint("widgets", "scroll_timeout")
    _scroll_distance = "{}sp".format(
        Config.getint("widgets", "scroll_distance")
    )


class ScrollView(StencilView):
    '''ScrollView class. See module documentation for more information.

    :Events:
        `on_scroll_start`
            Dispatch when scrolling is detected.
        `on_scroll_move`
            Dispatched when scrolling continues.
            Fires continuously during scrolling.
        `on_scroll_stop`
            Dispatched when scrolling stops. Fires when velocity reaches zero
            and scroll position stabilizes for 3 consecutive frames.

    .. versionchanged:: VERSION_NEXT
        `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are
        now dispatched for nested and non-nested ScrollViews. The behavior
        has been updated.  Previously these events mirrored the behavior of the
        on_touch_* methods.
        Now they fire when scrolling starts, continues and stops.

    .. versionchanged:: 1.9.0
        `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are
        now dispatched when scrolling to handle nested ScrollViews.

    .. versionchanged:: 1.7.0
        `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has
        been deprecated, use :attr:`effect_cls` instead.
    '''

    scroll_distance = NumericProperty(_scroll_distance)
    '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As
    soon as the distance has been traveled, the :class:`ScrollView` will start
    to scroll, and no touch event will go to children.
    It is advisable that you base this value on the dpi of your target device's
    screen.

    :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 20 (pixels), according to the default value in user
    configuration.
    '''

    scroll_wheel_distance = NumericProperty("20sp")
    '''Distance to move when scrolling with a mouse wheel.
    It is advisable that you base this value on the dpi of your target device's
    screen.

    .. versionadded:: 1.8.0

    :attr:`scroll_wheel_distance` is a
    :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels.
    '''

    scroll_timeout = NumericProperty(_scroll_timeout)
    '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds.
    If the user has not moved :attr:`scroll_distance` within the timeout,
    the scrolling will be disabled, and the touch event will go to the
    children.

    :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 55 (milliseconds) according to the default value in user
    configuration.

    .. versionchanged:: 1.5.0
        Default value changed from 250 to 55.
    '''

    scroll_x = NumericProperty(0.0)
    '''X scrolling value, between 0 and 1. If 0, the content's left side will
    touch the left side of the ScrollView. If 1, the content's right side will
    touch the right side.

    This property is controlled by :class:`ScrollView` only if
    :attr:`do_scroll_x` is True.

    :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    scroll_y = NumericProperty(1.0)
    '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will
    touch the bottom side of the ScrollView. If 1, the content's top side will
    touch the top side.

    This property is controlled by :class:`ScrollView` only if
    :attr:`do_scroll_y` is True.

    :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.
    '''

    do_scroll_x = BooleanProperty(True)
    '''Allow scroll on X axis.

    :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True.
    '''

    do_scroll_y = BooleanProperty(True)
    '''Allow scroll on Y axis.

    :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True.
    '''

    def _get_do_scroll(self):
        return (self.do_scroll_x, self.do_scroll_y)

    def _set_do_scroll(self, value):
        if isinstance(value, (list, tuple)):
            self.do_scroll_x, self.do_scroll_y = value
        else:
            self.do_scroll_x = self.do_scroll_y = bool(value)

    do_scroll = AliasProperty(
        _get_do_scroll,
        _set_do_scroll,
        bind=("do_scroll_x", "do_scroll_y"),
        cache=True,
    )
    '''Allow scroll on X or Y axis.

    :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of
    (:attr:`do_scroll_x` + :attr:`do_scroll_y`)
    '''

    always_overscroll = BooleanProperty(True)
    '''Make sure user can overscroll even if there is not enough content
    to require scrolling.

    This is useful if you want to trigger some action on overscroll, but
    there is not always enough content to trigger it.

    :attr:`always_overscroll` is a
    :class:`~kivy.properties.BooleanProperty` and defaults to `True`.

    .. versionadded:: 2.0.0

    The option was added and enabled by default, set to False to get the
    previous behavior of only allowing to overscroll when there is
    enough content to allow scrolling.
    '''

    def _get_vbar(self):
        # must return (y, height) in %
        # calculate the viewport size / scrollview size %
        if self._viewport is None:
            return 0, 1.0
        vh = self._viewport.height
        h = self.height
        if vh < h or vh == 0:
            return 0, 1.0
        ph = max(0.01, h / float(vh))
        sy = min(1.0, max(0.0, self.scroll_y))
        py = (1.0 - ph) * sy
        return (py, ph)

    vbar = AliasProperty(
        _get_vbar,
        bind=("scroll_y", "_viewport", "viewport_size", "height"),
        cache=True,
    )
    '''Return a tuple of (position, size) of the vertical scrolling bar.

    .. versionadded:: 1.2.0

    The position and size are normalized between 0-1, and represent a
    proportion of the current scrollview height. This property is used
    internally for drawing the little vertical bar when you're scrolling.

    :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
    '''

    def _get_hbar(self):
        # must return (x, width) in %
        # calculate the viewport size / scrollview size %
        if self._viewport is None:
            return 0, 1.0
        vw = self._viewport.width
        w = self.width
        if vw < w or vw == 0:
            return 0, 1.0
        pw = max(0.01, w / float(vw))
        sx = min(1.0, max(0.0, self.scroll_x))
        px = (1.0 - pw) * sx
        return (px, pw)

    hbar = AliasProperty(
        _get_hbar,
        bind=("scroll_x", "_viewport", "viewport_size", "width"),
        cache=True,
    )
    '''Return a tuple of (position, size) of the horizontal scrolling bar.

    .. versionadded:: 1.2.0

    The position and size are normalized between 0-1, and represent a
    proportion of the current scrollview height. This property is used
    internally for drawing the little horizontal bar when you're scrolling.

    :attr:`hbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
    '''

    bar_color = ColorProperty([0.7, 0.7, 0.7, 0.9])
    '''Color of horizontal / vertical scroll bar, in RGBA format.

    .. versionadded:: 1.2.0

    :attr:`bar_color` is a :class:`~kivy.properties.ColorProperty` and defaults
    to [.7, .7, .7, .9].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    bar_inactive_color = ColorProperty([0.7, 0.7, 0.7, 0.2])
    '''Color of horizontal / vertical scroll bar (in RGBA format), when no
    scroll is happening.

    .. versionadded:: 1.9.0

    :attr:`bar_inactive_color` is a
    :class:`~kivy.properties.ColorProperty` and defaults to [.7, .7, .7, .2].

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    bar_width = NumericProperty("2dp")
    '''Width of the horizontal / vertical scroll bar. The width is interpreted
    as a height for the horizontal bar.

    .. versionadded:: 1.2.0

    :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 2.
    '''

    bar_pos_x = OptionProperty("bottom", options=("top", "bottom"))
    '''Which side of the ScrollView the horizontal scroll bar should go
    on. Possible values are 'top' and 'bottom'.

    .. versionadded:: 1.8.0

    :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`,
    defaults to 'bottom'.

    '''

    bar_pos_y = OptionProperty("right", options=("left", "right"))
    '''Which side of the ScrollView the vertical scroll bar should go
    on. Possible values are 'left' and 'right'.

    .. versionadded:: 1.8.0

    :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'right'.

    '''

    bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y)
    '''Which side of the scroll view to place each of the bars on.

    :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`bar_pos_x`, :attr:`bar_pos_y`)
    '''

    bar_margin = NumericProperty(0)
    '''Margin between the bottom / right side of the scrollview when drawing
    the horizontal / vertical scroll bar.

    .. versionadded:: 1.2.0

    :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default
    to 0
    '''

    effect_cls = ObjectProperty(DampedScrollEffect, allownone=True)
    '''Class effect to instantiate for X and Y axis.

    .. versionadded:: 1.7.0

    :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to :class:`DampedScrollEffect`.

    .. versionchanged:: 1.8.0
        If you set a string, the :class:`~kivy.factory.Factory` will be used to
        resolve the class.

    '''

    effect_x = ObjectProperty(None, allownone=True)
    '''Effect to apply for the X axis. If None is set, an instance of
    :attr:`effect_cls` will be created.

    .. versionadded:: 1.7.0

    :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None.
    '''

    effect_y = ObjectProperty(None, allownone=True)
    '''Effect to apply for the Y axis. If None is set, an instance of
    :attr:`effect_cls` will be created.

    .. versionadded:: 1.7.0

    :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None, read-only.
    '''

    viewport_size = ListProperty([0, 0])
    '''(internal) Size of the internal viewport. This is the size of your only
    child in the scrollview.
    '''

    scroll_type = OptionProperty(
        ["content"],
        options=(
            ["content"],
            ["bars"],
            ["bars", "content"],
            ["content", "bars"],
        ),
    )
    '''Sets the type of scrolling to use for the content of the scrollview.
    Available options are: ['content'], ['bars'], ['bars', 'content'].

    +---------------------+------------------------------------------------+
    | ['content']         | Content is scrolled by dragging or swiping the |
    |                     | content directly.                              |
    +---------------------+------------------------------------------------+
    | ['bars']            | Content is scrolled by dragging or swiping the |
    |                     | scroll bars.                                   |
    +---------------------+------------------------------------------------+
    | ['bars', 'content'] | Content is scrolled by either of the above     |
    |                     | methods.                                       |
    +---------------------+------------------------------------------------+

    .. versionadded:: 1.8.0

    :attr:`scroll_type` is an :class:`~kivy.properties.OptionProperty` and
    defaults to ['content'].
    '''

    smooth_scroll_end = NumericProperty(None, allownone=True)
    '''Whether smooth scroll end should be used when scrolling with the
    mouse-wheel and the factor of transforming the scroll distance to
    velocity. This option also enables velocity addition meaning if you
    scroll more, you will scroll faster and further. The recommended value
    is `10`. The velocity is calculated as :attr:`scroll_wheel_distance` *
    :attr:`smooth_scroll_end`.

    .. versionadded:: 1.11.0

    :attr:`smooth_scroll_end` is a :class:`~kivy.properties.NumericProperty`
    and defaults to None.
    '''

    slow_device_support = BooleanProperty(False)
    '''Enable slow device support for scroll gesture detection.

    On very slow devices, at least 3 frames are needed to accumulate
    velocity data for scroll effects to work properly. When enabled,
    after the scroll timeout expires, the gesture handoff will be delayed
    until at least 3 frames have rendered, ensuring sufficient velocity
    data accumulation.

    This addresses issues #1464 and #1499 for low-performance devices.
    Disable this on modern hardware to improve touch responsiveness.

    :attr:`slow_device_support` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to False.

    .. versionadded:: NEXT_VERSION
    '''
    parallel_delegation = BooleanProperty(True)
    '''Controls boundary delegation behavior for parallel nested ScrollViews.

    When True (default, web-style):
