#!/usr/bin/env python3

"""
The base class from which all widgets derive.
"""

import time
import pyglet
import autoprop

from vecrec import Rect
from glooey import drawing
from glooey.helpers import *

class EventDispatcher(pyglet.event.EventDispatcher):
    """
    An extension of `pyglet.event.EventDispatcher` class that adds support for 
    continuously firing events and relaying events from other dispatchers.
    """

    def __init__(self):
        super().__init__()
        self.__timers = {}

    def relay_events_from(self, originator, event_type, *more_event_types):
        """
        Configure this handler to re-dispatch events from another handler.

        This method configures this handler dispatch an event of type 
        *event_type* whenever *originator* dispatches events of the same type 
        or any of the types in *more_event_types*.  Any arguments passed to the 
        original event are copied to the new event.

        This method is mean to be useful for creating composite widgets that 
        want to present a simple API by making it seem like the events being 
        generated by their children are actually coming from them.  See the 
        `/composing_widgets` tutorial for an example.
        """
        handlers = {
                event_type: lambda *args, **kwargs: \
                    self.dispatch_event(event_type, *args, **kwargs)
                for event_type in (event_type,) + more_event_types
        }
        originator.set_handlers(**handlers)

    def start_event(self, event_type, *args, dt=1/60):
        """
        Begin dispatching the given event at the given frequency.

        Calling this method will cause an event of type *event_type* with 
        arguments *args* to be dispatched every *dt* seconds.  This will 
        continue until `stop_event()` is called for the same event.

        These continuously firing events are useful if, for example, you want 
        to make a button that scrolls for as long as it's being held.
        """
        # Don't bother scheduling a timer if nobody's listening.  This isn't 
        # great from a general-purpose perspective, because a long-lived event 
        # could have listeners attach and detach in the middle.  But I don't 
        # like the idea of making a bunch of clocks to spit out a bunch of 
        # events that are never used, although to be fair I don't actually know 
        # how expensive that would be.  If I want to make this implementation 
        # more general purpose, I could start and stop timers as necessary in 
        # the methods that add or remove handlers.
        if not any(self.__yield_handlers(event_type)):
            return

        def on_time_interval(dt): #
            self.dispatch_event(event_type, *args, dt)

        pyglet.clock.schedule_interval(on_time_interval, dt)
        self.__timers[event_type] = on_time_interval

    def stop_event(self, event_type):
        """
        Stop dispatching the given event.

        It is not an error to attempt to stop an event that was never started, 
        the request will just be silently ignored.
        """
        if event_type in self.__timers:
            pyglet.clock.unschedule(self.__timers[event_type])

    def __yield_handlers(self, event_type):
        """
        Yield all the handlers registered for the given event type.
        """
        if event_type not in self.event_types:
            raise ValueError("%r not found in %r.event_types == %r" % (event_type, self, self.event_types))

        # Search handler stack for matching event handlers
        for frame in list(self._event_stack):
            if event_type in frame:
                yield frame[event_type]

        # Check instance for an event handler
        if hasattr(self, event_type):
            yield getattr(self, event_type)


@autoprop
@register_event_type(
    'on_attach',
    'on_detach',
    'on_attach_child',
    'on_detach_child',
    'on_repack',
    'on_regroup',
    'on_mouse_press',
    'on_mouse_release',
    'on_mouse_hold',
    'on_mouse_motion',
    'on_mouse_enter',
    'on_mouse_leave',
    'on_mouse_drag',
    'on_mouse_drag_enter',
    'on_mouse_drag_leave',
    'on_mouse_scroll',
    'on_click',
    'on_double_click',
    'on_rollover',
    'on_enable',
    'on_disable',
)
class Widget(EventDispatcher, HoldUpdatesMixin):
    """
    The base class from which all widgets derive.

    This class implements the features that are common to all widgets.  These 
    features include the ability to be drawn on the screen and assigned a size 
    and shape in the context of the rest of the GUI.  See `do_draw()`, 
    `do_claim()`, and `do_resize_children()`.  

    These features also include every widget's ability to control its size and 
    shape using padding, alignment, and size hints.  See the corresponding 
    getters, setters, and ``custom`` attributes.

    In addition, all widgets are capable of having children and propagating 
    mouse events to those children.  See `enable()`, `disable()`, 
    `is_under_mouse()`, and `_grab_mouse()`.
    """

    custom_alignment = 'fill'
    """
    How the widget should align itself within the space assigned to it.
    """

    custom_padding = None
    """
    How much space the widget should keep free around its edges.
    """

    custom_horz_padding = None
    """
    How much space the widget should keep free around its left and 
    right edges.

    This setting has priority over `custom_padding`.
    """

    custom_vert_padding = None
    """
    How much space the widget should keep free around its top and 
    bottom edges.

    This setting has priority over `custom_padding`.
    """

    custom_left_padding = None
    """
    How much space the widget should keep free on its left side.

    This setting has priority over `custom_horz_padding` and `custom_padding`.
    """

    custom_right_padding = None
    """
    How much space the widget should keep free on its right side.

    This setting has priority over `custom_horz_padding` and `custom_padding`.
    """

    custom_top_padding = None
    """
    How much space the widget should keep free on its top side.

    This setting has priority over `custom_vert_padding` and `custom_padding`.
    """

    custom_bottom_padding = None
    """
    How much space the widget should keep free on its bottom side.

    This setting has priority over `custom_vert_padding` and `custom_padding`.
    """

    custom_size_hint = 0, 0
    """
    The user-set minimum size for this widget.

    The widget also calculates its own minimum size, based on the content it 
    needs to fit within it.  The size hint can make the widget smaller than its 
    "internal" minimum size, but it can make it bigger.
    """

    custom_width_hint = None
    """
    The user-set minimum width for this widget.

    This setting has priority over `custom_size_hint`.
    """

    custom_height_hint = None
    """
    The user-set minimum height for this widget.

    This setting has priority over `custom_size_hint`.
    """

    custom_grab_mouse_on_click = False
    """
    Indicate that the widget should automatically grab the mouse when being 
    clicked.  This behavior is useful for widgets that emit interesting 
    ``on_hold`` events, or that can be dragged around.
    """

    custom_propagate_mouse_events = True
    """
    Whether or not this widget should propagate mouse events to its children.

    This is useful for implementing composite widgets that want to handle mouse 
    events without any duplication of effort or interference from their 
    children.
    """

    def __init__(self):
        """
        Initialize the widget.

        Don't forget to call this method from subclasses!
        """
        EventDispatcher.__init__(self)
        HoldUpdatesMixin.__init__(self)

        self.__root = None
        self.__parent = None
        self.__group = None

        # Use a double-underscore to avoid name conflicts; `__children` is a 
        # useful name for subclasses, so I don't want it to cause conflicts.
        self.__children = set()

        self.__children_under_mouse = set()
        self.__children_can_overlap = True
        self.__mouse_grabber = None

        # The amount of space requested by the user for this widget.
        self.__width_hint = first_not_none((
                self.custom_width_hint,
                self.custom_size_hint[0],
        ))
        self.__height_hint = first_not_none((
                self.custom_height_hint,
                self.custom_size_hint[1],
        ))
        # The minimal amount of space needed to display the content of this 
        # widget.  This is a combination of the size returned by the widget's 
        # do_claim() method and any size hints manually set by the user.
        self.__min_height = 0
        self.__min_width = 0

        # The minimum amount of space containers need to allocate for this 
        # widget.  This is just the size of the content plus any padding.
        self.__claimed_width = 0
        self.__claimed_height = 0
        self.__is_claim_stale = True

        # The space assigned to the widget by it's parent.  This cannot be 
        # smaller than self.claimed_rect, but it can be larger.
        self.__assigned_rect = None

        # The rect the widget should actually use to render itself.  This is 
        # determined by the alignment function from the content rect and the 
        # assigned rect.
        self.__rect = None

        # The rect the widget will actually show, plus any padding.  This is 
        # basically the claimed rect, but with meaningful coordinates.
        self.__padded_rect = None

        self.__is_hidden = False
        self.__is_parent_hidden = False
        self.__is_enabled = True

        # Attribute controlling mouse events.
        self.__grab_mouse_on_click = self.custom_grab_mouse_on_click
        self.__propagate_mouse_events = self.custom_propagate_mouse_events

        # Attributes for keeping track of the mouse-event related information, 
        # e.g. the rollover state and the double-click timer.
        self.__rollover_state = 'base'
        self.__last_rollover_state = 'base'
        self.__double_click_timer = 0

        # Take care to avoid calling any potentially polymorphic methods, such 
        # as set_padding() or _repack().  When these methods are overridden, 
        # they often become dependent on the overriding subclass being properly
        # initialized.  Since that hasn't happened by the time this constructor 
        # is called, these polymorphic methods can cause headaches.
        self.__set_padding(
                all=self.custom_padding,
                horz=self.custom_horz_padding,
                vert=self.custom_vert_padding,
                left=self.custom_left_padding,
                right=self.custom_right_padding,
                top=self.custom_top_padding,
                bottom=self.custom_bottom_padding,
        )
        self.__alignment = self.custom_alignment

    def __repr__(self):
        """
        Return a succinct string identifying this widget.

        The string includes the widget's class and a 4-letter identifier to 
        distinguish between different instances of the same class.  The 
        identifier is just the four least-significant hex digits of the 
        widget's address in memory, so while it is very unlikely that two 
        widget's would have the same identifier, it's not impossible.
        """
        return '{}(id={})'.format(
                self.__class__.__name__,
                hex(id(self))[-4:],
        )

    def __bool__(self):
        """
        Always consider widgets to be "true".

        This behavior is meant to facilitate comparisons against None.  This 
        method has to be explicitly implemented because otherwise python would 
        fallback on __len__(), which confusingly depends on whether or not the 
        widget has children.
        """
        return True

    def __len__(self):
        """
        Return the number of children this widget has.
        """
        return len(self.__children)

    def __contains__(self, widget):
        """
        Return true if the given widget is one of this widget's children.
        """
        return widget in self.__children

    def hide(self):
        """
        Make the widget invisible.

        This will undraw the widget and prevent it from being redrawn until 
        `unhide()` is called.  During that time, the widget will not be 
        visible, but it will still take up space in the GUI layout.  If that's 
        not what you want, you should remove the widget from it container 
        rather than simply hiding it.  It's safe to hide a widget that's 
        already hidden.
        """
        if self.is_visible:
            self._ungrab_mouse()
            self._undraw_all()
        self.__is_hidden = True
        self._hide_children()

    def unhide(self, draw=True):
        """
        Make the widget visible again.

        This just undoes the effects of `hide()` and make the widget behave 
        like normal again.  It's safe to unhide a widget that's already 
        unhidden.
        """
        self.__is_hidden = False
        if self.is_visible and draw:
            self._draw_all()
        self._unhide_children(draw)

    def enable(self):
        """
        Indicate that the widget should react to the mouse.

        In particular, this means enabling rollover and click/double-click 
        events.  Most widgets are enabled by default.
        """
        self.__is_enabled = True
        self.dispatch_event('on_enable', self)

    def disable(self):
        """
        Prevent the widget from reacting to the mouse.

        In particular, this means disabling rollover and click/double-click 
        events.
        """
        self.__is_enabled = False
        self.dispatch_event('on_disable', self)

    def do_attach(self):
        """
        React to the widget being attached to the GUI.

        Specifically, this method is called when the widget becomes connected, 
        through any number of parent widgets, to the root of the widget 
        hierarchy.  When this happens, ``self.root`` will return a widget 
        rather than None.
        
        Note that this method will not necessarily be called when the widget is 
        simply added to a parent widget.  If the parent is already attached to 
        the GUI itself, then this method will be called right then.  Otherwise, 
        this method will be called when that parent (or one of its parents) is 
        attached to the GUI.

        This method is mainly useful for widgets that need to take control away 
        from the root widget for some reason.  For example, Viewport widgets 
        override how mouse events are interpreted and Dialog widgets grab all 
        the key and mouse events for themselves.
        """
        pass

    def do_detach(self):
        """
        React to the widget being detached from the GUI.
        """
        pass

    def do_claim(self):
        """
        Return the minimum width and height needed to render this widget.

        Most widgets need to implement this method.  The exception is widgets 
        that have exactly one child.  In that case, there's a reasonable 
        default: claim just enough space for that child.   Most composite 
        widgets are covered by this default, because they typically have a 
        container widget as their only child and attach any other widgets they 
        need to that container.
        """
        if self.__num_children == 1:
            return next(iter(self.__children)).claimed_size
        else:
            raise NotImplementedError

    def do_resize(self):
        """
        React to a change in the widget's size.

        Only widgets that actually draw things need to implement this method.  
        If the widget has children widgets that it may need to redistribute 
        space between, it should do that in `do_resize_children()`.

        However, keep in mind that this method is called before the widget is 
        drawn and may be called after it's been undrawn, so any vertex lists 
        created in the draw function may or may not exist yet/anymore.  If 
        those vertex lists don't exist yet, there's nothing this function needs 
        to do.  The `_draw()` function will be called when the widget's ready 
        to draw, and at that point the vertex lists should be created with the 
        right size and shape.
        """
        pass

    def do_resize_children(self):
        """
        React to changes that might require this widget's children to be 
        resized.

        This method is called when the widget's own size is changed or when 
        children are attached to or detached from the widget.  A typical 
        implementation would iterate through all the children attached to the 
        widget and call `_resize()` on each one.
        """
        for child in self.__children:
            child._resize(self.rect)

    def do_regroup(self):
        """
        React to a change in the widget's pyglet graphics group.

        In pyglet, groups are used to control layers and OpenGL state.  This 
        method is called whenever the widget's group is changed, for example 
        when the widget is attached to the GUI or moved from one part 
        of the GUI to another.

        Only widgets that actually draw things need to implement this method, 
        because groups aren't used for anything but drawing.  Widgets that 
        contain other widgets may need to implement `do_regroup_children()` 
        to describe how those children should be regrouped.

        It's not always trivial to change the group used to draw something in 
        pyglet.  The way to do this depends on what's being drawn and whether 
        or not it's been drawn before.  The simplest case are high-level APIs 
        like `pyglet.sprite.Sprite` that allow you to simply change a 
        ``group`` attribute.  On the other hand, if you're drawing vertex lists 
        yourself, you need to call the `pyglet.graphics.Batch.migrate()` 
        method.  This method needs to know the OpenGL mode (e.g. GL_QUADS) 
        associated with the vertex list, so you will have to keep track of 
        that.

        Keep in mind that this method is called before the widget is drawn and 
        may be called after it's been undrawn, so any vertex lists created in 
        the draw function may or may not exist yet/anymore.  If those vertex 
        lists don't exist yet, there's nothing this function needs to do.  The 
        `_draw()` function will be called when the widget's ready to draw, and 
        at that point the vertex lists should be created with the right group.
        """
        self._undraw()

    def do_regroup_children(self):
        """
        React to changes that might require this widget's children to be 
        regrouped.

        This method is called when the widget's own group is changed or when 
        children are attached to or detached from the widget.  A typical 
        implementation would iterate through all the children attached to the 
        widget and call `_regroup()` on each one.  The default implementation 
        puts all the children in the same group as the widget itself.
        """
        for child in self.__children:
            child._regroup(self.group)

    def do_draw(self):
        """
        Draw any shapes or images associated with this widget.

        This method is called by `_draw()` after it checks to make sure the 
        widget is attached to the root of the GUI hierarchy and that the 
        `rect`, `group`, and `batch` attributes have all been set.  

        This method is called both to draw the widget for the first time and to 
        update it subsequently.  This usually means that you need to check to 
        see if your graphics objects and resources need to be initialized yet.
        """
        pass

    def do_undraw(self):
        """
        Delete any shapes or images associated with this widget.
        This method may be called before _draw().
        """
        pass

    def do_find_children_near_mouse(self, x, y):
        """
        Yield all the children that could be under the given mouse coordinate.
        
        The order in which the children are yielded has no significance.  It's 
        ok to yield children that are hidden or are not actually under the 
        given coordinate; `__find_children_under_mouse()` will check these 
        things for every widget produced by this method.  However, failing to 
        yield a child that actually is under the mouse will result in that 
        child not responding to the mouse.
        
        The default implementation just yields all of the widgets children, but 
        subclasses may be able to use knowledge of their geometry to quickly 
        yield a smaller set of children to check.  `Grid` is a good example of 
        a widget that does this.
        """
        yield from self.__children

    def on_mouse_press(self, x, y, button, modifiers):
        """
        React when the mouse is pressed on this widget.

        The ``on_mouse_press` event is propagated to any children under the 
        mouse, an ``on_mouse_hold`` event is started, and an ``on_rollover`` 
        event is fired to indicate that the widget's rollover state is now 
        "down".
        """
        # Propagate the "on_mouse_press" event to the relevant children.
        children_under_mouse = self.__find_children_under_mouse(x, y)

        if self.propagate_mouse_events:
            for child in children_under_mouse.current:
                child.dispatch_event('on_mouse_press', x, y, button, modifiers)

        # Start firing "on_mouse_hold" events every frame.
        self.start_event('on_mouse_hold')

        if self.grab_mouse_on_click:
            self._grab_mouse()

        # Update the widget's rollover state.
        self.__update_rollover_state('down', x, y)

    def on_mouse_release(self, x, y, button, modifiers):
        """
        React when the mouse is released over this widget.

        The ``on_mouse_release` event is propagated to any children under the 
        mouse, any ``on_mouse_hold`` event is stopped, an ``on_click`` event is 
        fired if the click started and stopped without leaving the widget, and 
        ``on_double_click`` event is triggered if the widget was clicked twice 
        in the last 500 ms, and an ``on_rollover`` event is fired to indicate 
        that the widget's rollover state is now "over".
        """
        # Propagate the "on_mouse_release" event to the relevant children.
        children_under_mouse = self.__find_children_under_mouse(x, y)

        if self.propagate_mouse_events:
            for child in children_under_mouse.current:
                child.dispatch_event('on_mouse_release', x, y, button, modifiers)

        # Stop firing "on_mouse_hold" events every frame.
        self.stop_event('on_mouse_hold')
        
        if self.grab_mouse_on_click:
            self._ungrab_mouse()

        # Decide whether to emit 'on_click' or 'on_double_click' events.  A 
        # click requires that the widget is enabled (i.e. not greyed out) and 
        # that user user pressed and released the mouse without leaving the 
        # widget.  A double click requires that the user do all of that twice 
        # within 500 ms.
        if self.__is_enabled and self.__rollover_state == 'down':
            self.dispatch_event('on_click', self)

            if time.perf_counter() - self.__double_click_timer < 0.5:
                self.dispatch_event('on_double_click', self)
                self.__double_click_timer = 0
            else:
                self.__double_click_timer = time.perf_counter()

        # Update the widget's rollover state.
        self.__update_rollover_state('over', x, y)

    def on_mouse_motion(self, x, y, dx, dy):
        """
        React to the mouse moving within this widget.

        The ``on_mouse_motion`` event is propagated to any children that 
        remained under the mouse, an ``on_mouse_enter`` event is triggered in 
        any children that just came under the mouse, and an ``on_mouse_leave`` 
        event is triggered in any children that were previously under the mouse 
        but no longer are.
        """
        children_under_mouse = self.__find_children_under_mouse(x, y)

        if self.propagate_mouse_events:
            for child in children_under_mouse.exited:
                child.dispatch_event('on_mouse_leave', x, y)

            for child in children_under_mouse.entered:
                child.dispatch_event('on_mouse_enter', x, y)

            for child in children_under_mouse.current:
                child.dispatch_event('on_mouse_motion', x, y, dx, dy)

    def on_mouse_enter(self, x, y):
        """
        React to the mouse crossing into this widget.

        The ``on_mouse_enter`` event is propagated to any children under the 
        mouse and an ``on_rollover`` event is triggered to indicate that the 
        widget's rollover state is now "over".
        """
        # Propagate the "on_mouse_enter" event to the relevant children.
        children_under_mouse = self.__find_children_under_mouse(x, y)

        if self.propagate_mouse_events:
            for child in children_under_mouse.entered:
                child.dispatch_event('on_mouse_enter', x, y)

        # Update the widget's rollover state.
        self.__update_rollover_state('over', x, y)
        
    def on_mouse_leave(self, x, y):
        """
        React to the mouse crossing out of this widget.

        The ``on_mouse_leave`` event is propagated to any children that are no 
        longer under the mouse (i.e. that aren't grabbing the mouse) and an 
        ``on_rollover`` event is triggered to indicate that the widget's 
        rollover state is now "base".
        """
        # Propagate the "on_mouse_leave" event to the relevant children.  We 
        # have to actually check which widgets are still "under the mouse" to 
        # correctly handle widgets that are grabbing the mouse when the mouse 
        # leaves the window.  (Previously I thought it was safe to assume that 
        # no children were under the mouse here, but it's not.)
        children_under_mouse = self.__find_children_under_mouse_after_leave()

        if self.propagate_mouse_events:
            for child in children_under_mouse.exited:
                child.dispatch_event('on_mouse_leave', x, y)

        # Stop firing "on_mouse_hold" events every frame.
        self.stop_event('on_mouse_hold')

        # Update the widget's rollover state.
        self.__update_rollover_state('base', x, y)

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        """
        React to the mouse being dragged within this widget.

        The ``on_mouse_drag`` event is propagated to any children that remained 
        under the mouse, an ``on_mouse_drag_enter`` event is triggered in any 
        children that just came under the mouse, and an ``on_mouse_drag_leave`` 
        event is triggered in any children that were previously under the mouse 
        but no longer are.
        """
        children_under_mouse = self.__find_children_under_mouse(x, y)

        if self.propagate_mouse_events:
            for child in children_under_mouse.exited:
                child.dispatch_event('on_mouse_drag_leave', x, y)

            for child in children_under_mouse.entered:
                child.dispatch_event('on_mouse_drag_enter', x, y)

            for child in children_under_mouse.current:
                child.dispatch_event('on_mouse_drag', x, y, dx, dy, buttons, modifiers)

    def on_mouse_drag_enter(self, x, y):
        """
        React to the mouse being dragged into this widget.

        The ``on_mouse_drag_enter`` event is propagated to any children under 
        the mouse.
        """
        children_under_mouse = self.__find_children_under_mouse(x, y)

        if self.propagate_mouse_events:
            for child in children_under_mouse.entered:
                child.dispatch_event('on_mouse_drag_enter', x, y)

    def on_mouse_drag_leave(self, x, y):
        """
        React to the mouse being dragged out of this widget.

        The ``on_mouse_drag_leave`` event is propagated to any children that 
        are no longer under the mouse (i.e. that aren't grabbing the mouse), 
        any ``on_mouse_hold`` event is stopped, and an ``on_rollover`` event is 
        triggered to indicate that the widget's rollover state is now "base".
        """
        # Propagate the "on_mouse_drag_leave" event to the relevant children.  
        # We have to actually check which widgets are still "under the mouse" 
        # to correctly handle widgets that are grabbing the mouse when the 
        # mouse leaves the window.
        children_under_mouse = self.__find_children_under_mouse_after_leave()

        if self.propagate_mouse_events:
            for child in children_under_mouse.exited:
                child.dispatch_event('on_mouse_drag_leave', x, y)

        # Stop firing "on_mouse_hold" events every frame.
        self.stop_event('on_mouse_hold')

        # Reset the rollover to its base state if the user drags the mouse 
        # outside of the widget.
        self.__update_rollover_state('base', x, y)

    def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
        """
        React to the mouse scroll wheel being turned.

        The ``on_mouse_scroll`` event is propagated to any children under the 
        mouse.
        """
        children_under_mouse = self.__find_children_under_mouse(x, y)

        if self.propagate_mouse_events:
            for child in children_under_mouse.current:
                child.dispatch_event('on_mouse_scroll', x, y, scroll_x, scroll_y)

    def get_parent(self):
        """
        Return this widget's parent, or None if this widget hasn't been 
        attached to the GUI yet.
        """
        return self.__parent

    def get_root(self):
        """
        Return the top of the widget hierarchy, or None if this widget hasn't 
        been attached to the GUI yet.
        """
        return self.__root

    def get_window(self):
        """
        Return the `pyglet.window.Window` that the GUI is part of, or None if 
        this widget hasn't been attached to the GUI yet.

        You can attach event handlers to the window to react to events that 
        aren't propagated through the widget hierarchy, e.g. key presses.
        """
        return self.root.window if self.is_attached_to_gui else None

    def get_batch(self):
        """
        Return the `pyglet.graphics.Batch` object being used to render the GUI, 
        or None if this widget hasn't been attached to the GUI yet.

        You often need to interact with the batch when implementing 
        `do_draw()`.
        """

        return self.root.batch if self.is_attached_to_gui else None

    def get_group(self):
        """
        Return the `pyglet.graphics.Group` object being used to render the GUI, 
        or None if this widget hasn't been attached to the GUI yet.

        You often need to use the group when implementing `do_draw()`, 
        `do_regroup()`, and `do_regroup_children()`.
        """
        return self.__group

    def get_rect(self):
        """
        Return the `vecrec.Rectangle` indicating where this widget is located 
        on the screen.

        This rectangle already accounts for the widget's padding, alignment, 
        and size hint, so when drawing you should just try to fill the whole 
        rectangle.
        """
        return self.__rect

    def get_width(self):
        """
        Return the width of the widget.
        """
        return self.__rect.width

    def get_height(self):
        """
        Return the height of the widget.
        """
        return self.__rect.height

    def get_size_hint(self):
        """
        Return the user-set minimum dimensions of this widget.

        The widget cannot be smaller than either the claim (which is 
        calculated internally by the widget based on its own geometry) of the 
        size hint (which can be explicitly provided by the developer).
        """
        return self.__width_hint, self.__height_hint

    def set_size_hint(self, new_width, new_height):
        """
        Set the minimum size for this widget.

        The widget cannot be smaller than either the claim (which is 
        calculated internally by the widget based on its own geometry) of the 
        size hint (which can be explicitly provided by the developer).
        """
        self.__width_hint = new_width
        self.__height_hint = new_height
        self._repack()

    def get_width_hint(self):
        """
        Return the user-set minimum width for this widget.
        """
        return self.__width_hint

    def set_width_hint(self, new_width):
        """
        Set the minimum width for this widget.
        """
        self.__width_hint = new_width
        self._repack()

    def get_height_hint(self):
        """
        Return the user-set minimum height for this widget.
        """
        return self.__height_hint

    def set_height_hint(self, new_height):
        """
        Set the minimum height for this widget.
        """
        self.__height_hint = new_height
        self._repack()

    def get_claimed_rect(self):
        """
        Return a `vecrec.Rectangle` indicating the amount of space needed to 
        render this widget.

        Only the width and height of the rectangle matter, the position is 
        unimportant (and will in fact always be such that the lower left corner 
        is at the origin).
        """
        return Rect.from_size(self.__claimed_width, self.__claimed_height)

    def get_claimed_size(self):
        """
        Return the width and height needed to render this widget.
        """
        return self.__claimed_width, self.__claimed_height

    def get_claimed_width(self):
        """
        Return the width needed to render this widget.
        """
        return self.__claimed_width

    def get_claimed_height(self):
        """
        Return the width needed to render this widget.
        """
        return self.__claimed_height

    def get_padded_rect(self):
        """
        Return a `vecrec.Rectangle` indicating the amount of space needed for 
        the widget's content plus its padding.

        This information is useful to container widgets, which need to work out 
        how to arrange their children.
        """
        return self.__padded_rect

    def get_padding(self):
        """
        Return the padding on all sides of this widget, as a (left, right, top 
        bottom) tuple.
        """
        return (self.__left_padding, self.__right_padding,
                self.__top_padding, self.__bottom_padding)

    def set_padding(self, all=None, *, horz=None, vert=None,
            left=None, right=None, top=None, bottom=None):
        """
        Set the padding for any or all sides of this widget.
        """
        self.__set_padding(all, horz, vert, left, right, top, bottom)
        self._repack()
    def get_horz_padding(self):
        """
        Return the padding on the left and right sides of this widget.
        """
        return self.__left_padding, self.__right_padding

    def set_horz_padding(self, new_padding):
        """
        Set the padding for the left and right sides of this widget.
        """
        self.__left_padding = new_padding
        self.__right_padding = new_padding
        self._repack()

    def get_vert_padding(self):
        """
        Return the padding on the top and bottom of this widget.
        """
        return self.__top_padding, self.__bottom_padding

    def set_vert_padding(self, new_padding):
        """
        Set the padding for the top and bottom of this widget.
        """
        self.__top_padding = new_padding
        self.__bottom_padding = new_padding
        self._repack()

    def get_left_padding(self):
        """
        Return the padding on the left side of this widget.
        """
        return self.__left_padding

    def set_left_padding(self, new_padding):
        """
        Set the padding for the left side of this widget.
        """
        self.__left_padding = new_padding
        self._repack()

    def get_right_padding(self):
        """
        Return the padding on the right side of this widget.
        """
        return self.__right_padding

    def set_right_padding(self, new_padding):
        """
        Set the padding for the right side of this widget.
        """
        self.__right_padding = new_padding
        self._repack()

    def get_top_padding(self):
        """
        Return the padding on the top of this widget.
        """
        return self.__top_padding

    def set_top_padding(self, new_padding):
        """
        Set the padding for the top of this widget.
        """
        self.__top_padding = new_padding
        self._repack()

    def get_bottom_padding(self):
        """
        Return the padding on the bottom of this widget.
        """
        return self.__bottom_padding

    def set_bottom_padding(self, new_padding):
        """
        Set the padding for the bottom of this widget.
        """
        self.__bottom_padding = new_padding
        self._repack()

    def get_total_padding(self):
        """
        Return the combined padding for both dimensions of this widget, as a 
        (horizontal, vertical) tuple.
        """
        return self.total_horz_padding, self.total_vert_padding

    def get_total_horz_padding(self):
        """
        Return the sum of the padding on the left and right sides of this 
        widget.
        """
        return self.__left_padding + self.__right_padding

    def get_total_vert_padding(self):
        """
        Return the sum of the padding on the top and bottom of this widget.
        """
        return self.__top_padding + self.__bottom_padding

    def get_alignment(self):
        """
        Return how this widget will be positioned within the space assigned to 
        it.

        The alignment can be either a string or a function.  For more 
        information, see the tutorial on `padding_alignment_size_hints` or the 
        API documentation for the `glooey.drawing.alignment` module.
        """
        return self.__alignment

    def set_alignment(self, new_alignment):
        """
        Set how this widget will be positioned within the space assigned to 
        it.

        The alignment can be either a string or a function.  For more 
        information, see the tutorial on `padding_alignment_size_hints` or the 
        API documentation for the `glooey.drawing.alignment` module.
        """
        self.__alignment = new_alignment
        self._repack()

    def get_rollover_state(self):
        """
        Return a string specifying how the mouse is currently interacting with 
        this widget.

        The interactions between each widget and the mouse are recalculated 
        each time the mouse moves, i.e. on every ``on_mouse_motion`` event.
        The following rollover states exist:

        "base"
            The mouse is not interacting with the widget.
        "over"
            The mouse is on top of the widget.
        "down"
            The mouse is being pressed on the widget.  If the mouse is 
            released while in this state, an ``on_click`` event will be 
            triggered.
        """
        return self.__rollover_state

    def get_last_rollover_state(self):
        """
        Return a string specifying how the mouse was interacting with this 
        widget before its most recent motion.

        See `get_rollover_state()` for the list of possible rollover states.
        """
        return self.__last_rollover_state

    def get_grab_mouse_on_click(self):
        return self.__grab_mouse_on_click

    def set_grab_mouse_on_click(self, new_setting):
        self.__grab_mouse_on_click = new_setting

    def get_propagate_mouse_events(self):
        """
        Return whether or not this widget will propagate mouse events to its 
        children.
        """
        return self.__propagate_mouse_events

    def set_propagate_mouse_events(self, new_setting):
        """
        Set whether or not this widget will propagate mouse events to its 
        children.
        """
        self.__propagate_mouse_events = new_setting

    @property
    def is_root(self):
        """
        True if this is the root of the widget hierarchy.
        """
        return self.root is self

    @property
    def is_hidden(self):
        """
        True if this widgets or one of its parents is hidden.

        See `hide()` for a precise description of what it means for a widget to 
        be hidden.
        """
        return self.__is_hidden or self.__is_parent_hidden

    @property
    def is_visible(self):
        """
        False if this widgets or one of its parents is hidden.

        This property is just the opposite of `~Widget.is_hidden`.
        """
        return not self.is_hidden

    @property
    def is_enabled(self):
        """
        True if the widget can be clicked on.

        See `enable()` for a precise description of what it means for a widget 
        to be enabled.
        """
        return self.__is_enabled

    @property
    def is_disabled(self):
        """
        True if the widget can be clicked on.

        This property is just the opposite of `~Widget.is_enabled`.
        """
        return not self.is_enabled

    @property
    def is_attached_to_gui(self):
        """
        True if the widget is part of the widget hierarchy.
        """
        return self.root is not None

    def is_under_mouse(self, x, y):
        """
        Return true if the widget is under the given mouse coordinate.

        The default implementation just checks the given coordinate against the 
        widget's rectangle, but you can reimplement this method in subclasses 
        to support other geometries.
        """
        if self.rect is None:
            return False
        if self.parent and self.parent.__mouse_grabber is self:
            return True
        return (x, y) in self.rect

    def debug_drawing_problems(self):
        """
        Suggest reasons why a widget is not displaying.

        It can be hard to debug problems when nothing is showing up on the 
        screen, so this method is meant to help look for common reasons why 
        that might be the case.  The suggestions will be printed to stdout.  
        Make sure to call this method after attaching the widget to the GUI 
        hierarchy, otherwise you'll just get a message saying the widget hasn't 
        been attached to the GUI yet.
        """
        diagnoses = []

        # If the widget isn't attached to the GUI, figure out which parent is 
        # the problem.

        if not self.is_attached_to_gui:

            def find_unattached_parent(widget, level=0): #
                if widget.parent is None: return widget, level
                if widget.parent is self.root: return widget, level
                else: return find_unattached_parent(widget.parent, level + 1)

            try:
                unattached_parent, level = find_unattached_parent(self)
            except RecursionError:
                diagnoses.append("{self} seems to have an infinite number of parents.\nCheck for circular references between its parents.")
            else:
                if unattached_parent is self:
                    diagnoses.append("{self} is not attached to the GUI.")
                else:
                    diagnoses.append("{unattached_parent}, a widget {level} level(s) above {self}, is not attached to the GUI.")

        # If the widget is attached to the GUI, make sure the widget is fully 
        # configured (i.e. rect and group are set) and not hidden.

        else:
            if self.rect is None:
                diagnoses.append("{self} was not given a size by its parent.\nCheck for bugs in {self.parent.__class__.__name__}.do_resize_children()")

            elif self.rect.area == 0:
                if self.claimed_width and self.claimed_height:
                    diagnoses.append("{self} requested {self.claimed_width}x{self.claimed_height} but was given {self.rect.width}x{self.rect.height}.\nCheck for bugs in {self.parent.__class__.__name__}.do_resize_children()")
                else:
                    diagnoses.append("{self} was given no space because it requested {self.claimed_width}x{self.claimed_height}.\nCheck for bugs in {self.__class__.__name__}.do_claim()")

            if self.group is None:
                diagnoses.append("{self} was not given a group by its parent.\nCheck for bugs in {self.parent.__class__.__name__}.do_regroup_children()")

            if self.is_hidden:

                def find_hidden_parent(widget, level=0): #
                    if widget._hidden: return widget, level
                    else: return find_hidden_parent(widget.parent, level + 1)

                hidden_parent, level = find_hidden_parent(self)

                if hidden_parent is self:
                    diagnoses.append("{self} is hidden.\nCall {self.__class__.__name__}.unhide() to reveal it.")
                else:
                    diagnoses.append("{hidden_parent}, a widget {level} level(s) above {self}, is hidden.\nCall {hidden_parent.__class__.__name__}.unhide() to reveal it and its children.")

        # If no problems were found, say so.

        if not diagnoses:
            diagnoses.append("{self} seems to have been drawn.\nCheck for bugs in {self.__class__.__name__}.do_draw()")

        # Print out the diagnoses.

        def join(items, sep): #
            for i, item in enumerate(items):
                if i < len(items) - 1:
                    yield item, sep
                else:
                    yield item, ''

        for diagnosis, sep in join(diagnoses, '\n'):
            print(diagnosis.format(**locals()) + sep, flush=True)

    def debug_placement_problems(self, claimed='red', assigned='yellow',
            content='blue'):
        """
        Draw boxes showing the widgets assigned, claimed, and content 
        rectangles.

        The claimed rect will always appear in the bottom left corner, because 
        the claim is only a size, not a position.  Sometimes the assigned 
        rectangle can obscure the content rectangle, so be aware that that 
        could happen.
        """
        if not self.is_attached_to_gui:
            raise UsageError("the widget must be attached to the GUI to debug placement problems.")

        layer = pyglet.graphics.OrderedGroup(
                len(self.root.layers) + 1, self.root.group)

        drawing.Outline(
            rect=self.claimed_rect,
            color=claimed,
            batch=self.batch,
            group=pyglet.graphics.OrderedGroup(1, layer),
        )
        drawing.Outline(
            rect=self.__assigned_rect,
            color=assigned,
            batch=self.batch,
            group=pyglet.graphics.OrderedGroup(2, layer),
        )
        drawing.Outline(
            rect=self.rect,
            color=content,
            batch=self.batch,
            group=pyglet.graphics.OrderedGroup(3, layer),
        )

    @update_function
    def _repack(self):
        """
        Indicate that the widget's size may have changed, and that the sizes of 
        its children and parents may also need to change in response.

        This method triggers a recursive update that first goes down the widget 
        hierarchy figuring out how much space each widget needs, and then goes 
        up the hierarchy figuring out how much space each widget gets.

        The most common way to use this method is in setter functions in 
        `Widget` subclasses, where the attribute being set might change the 
        shape of the widget.  If the attribute being set might change the 
        widget's appearance, but *not* it's size, call `_draw()` instead.
        """
        if not self.is_attached_to_gui:
            return

        p()
        has_claim_changed = self._claim()

        # If the widget is a different size than it used to be, give its parent 
        # a chance to repack it.
        if has_claim_changed:
            self.__is_claim_stale = False
            self.parent._repack()
            self.__is_claim_stale = True

        # Otherwise, stop recursing and resize the widget's children.
        else:
            self._realign()

        self.dispatch_event('on_repack')

    def _claim(self):
        """
        Make sure the widget's claim is up-to-date.

        More specifically, this methods updates ``self.__claimed_width`` and 
        ``self.__claimed_height`` to reflect the current state of the widget and 
        its children, then returns whether or not the claim changed since the 
        last time this method was called.  If this function is called more than 
        once within a single repack (a common scenario because every widget 
        depends on the claim of all its children and grandchildren), the first 
        call will update the claim and any subsequent calls will simply return 
        False without recalculating anything.

        When the claim needs to be recalculated, the first step is the update 
        the claims made by all the widget's children, since this widget's claim 
        will depend on that information.  This is a recursive process that may 
        descend all the way down the widget hierarchy.  The next step is to 
        delegate the actual calculation of the minimum width and height needed 
        *for the contents of the widget (i.e. excluding padding)* to 
        `do_claim()`, which should be overridden in `Widget` subclasses.  
        Finally, the claim is modified to account for padding and the 
        corresponding private attributes are updated.

        This method should not be called outside of a repack.  If you're using 
        glooey to make a GUI, I can't think of a scenario where you should call 
        or override this method.
        """
        # Only calculate the claim once during each repack.
        if not self.__is_claim_stale:
            return False

        # Have each child widget claim space for itself, so this widget can 
        # take those space requirements into account.
        for child in self.__children:
            child._claim()

        # Make note of the previous claim, so we can say whether or not it has 
        # changed.
        previous_claim = self.__claimed_width, self.__claimed_height

        # Keep track of the amount of space the widget needs for itself (min_*) 
        # and for itself in addition to its padding (claimed_*).
        min_width, min_height = self.do_claim()

        self.__min_width = max(min_width, self.__width_hint)
        self.__min_height = max(min_height, self.__height_hint)

        self.__claimed_width = self.__min_width + self.total_horz_padding
        self.__claimed_height = self.__min_height + self.total_vert_padding

        # Return whether or not the claim has changed since the last repack.  
        # This determines whether the widget's parent needs to be repacked.
        return previous_claim != (self.__claimed_width, self.__claimed_height)

    def _resize(self, new_rect):
        """
        Change the size or shape of this widget.

        This method is triggered by _repack(), which recursively climbs the 
        widget hierarchy to make space for the widgets that need it, then calls 
        _resize() on any widget that need to adapt to the new space allocation.

        This method should not be called outside of a repack, because it 
        assumes that the claims have already been updated.
        """
        # Make sure the new size is still at least as big as the widget's 
        # claim.  Round down all the sizes when doing this comparison, because 
        # the new rect may also be rounded down.
        if int(new_rect.width) < int(self.claimed_width):
            raise UsageError(f"cannot assign {self} a smaller width ({new_rect.width} px) than it claimed ({self.claimed_width} px).")
        if int(new_rect.height) < int(self.claimed_height):
            raise UsageError(f"cannot assign {self} a smaller height ({new_rect.height} px) than it claimed ({self.claimed_height} px).")

        self.__assigned_rect = new_rect
        self._realign()

    def _realign(self):
        """
        Update the amount of space available for the widget's content, given 
        the amount of space assigned to it by its parent.

        Starting from the amount of space assigned to this widget by its parent, 
        this means subtracting the padding, performing the proper alignment, 
        and rounding to the nearest integer pixel to prevent seams from 
        appearing.  The final result is stored in ``self.__rect``.  

        Three callbacks are invoked to allow the widget to react to this 
        change: `do_resize()`, `do_draw()`, and `do_resize_children()`.  The 
        last initiates a recursive descent down the widget hierarchy updating 
        the sizes of the widget's children and all of their children, which is 
        a critical part of the repacking process.

        This method should not be called outside of a repack, because it 
        assumes that the claims have already been updated.
        """
        if self.__assigned_rect is None:
            return

        # Subtract padding from the full amount of space assigned to this 
        # widget.
        max_rect = self.__assigned_rect.copy()
        max_rect.left += self.left_padding
        max_rect.bottom += self.bottom_padding
        max_rect.width -= self.total_horz_padding
        max_rect.height -= self.total_vert_padding

        # Align this widget within the space available to it (i.e. the assigned 
        # space minus the padding).
        aligned_rect = Rect.from_size(self.__min_width, self.__min_height)
        drawing.align(self.__alignment, aligned_rect, max_rect)

        # Round the rectangle to the nearest integer pixel, because sometimes 
        # images can't line up right (e.g. in Background widgets) if the widget 
        # has fractional coordinates.
        aligned_rect.round()

        # Guarantee that do_resize() is only called if the size of the widget 
        # actually changed.  This is probably doesn't have a significant effect 
        # on performance, but hopefully it gives people reimplementing 
        # do_resize() less to worry about.
        if self.__rect is None or self.__rect != aligned_rect:
            self.__rect = aligned_rect
            self.__padded_rect = aligned_rect.copy()
            self.__padded_rect.left -= self.left_padding
            self.__padded_rect.bottom -= self.bottom_padding
            self.__padded_rect.width += self.total_horz_padding
            self.__padded_rect.height += self.total_vert_padding
            self.do_resize()

        # Repacking a widget should always cause it to be redrawn.  Widgets use 
        # `_repack()` to indicate that their size or appearance may have 
        # changed, so it's possible that only the appearance changed.  For 
        # example, this would happen if you replaced an image with another of 
        # the same size.  If the widget isn't ready to draw for some reason, 
        # `_draw()` won't do anything.
        self._draw()

        # The children may need to be resized even if this widget doesn't.  For 
        # example, consider a container that takes up the whole window.  It's 
        # size won't change when a widget is added or removed from it, but it's 
        # children will still need to be resized.
        if self.__num_children > 0:
            self.do_resize_children()

    def _regroup(self, new_group):
        """
        Change the `pyglet.graphics.Group` associated with this widget.

        Three callbacks are invoked to allow the widget to react to this 
        change: `do_regroup()`, `do_draw()`, and `do_regroup_children()`.  The 
        last initiates a recursive descent down the widget hierarchy updating 
        the groups of the widget's children and all of their children.
        """
        # Changing the group is often an expensive operation, so don't do 
        # anything unless we have to.  It is assumed that do_regroup_children() 
        # depends only on self.__group, so if self.__group doesn't change, 
        # self.do_regroup_children() doesn't need to be called.
        if self.__group is None or self.__group != new_group:
            self.__group = new_group
            self.do_regroup()

            # Try to redraw the widget.  This won't do anything if the widget 
            # isn't ready to draw.
            self._draw()

            if self.__num_children > 0:
                self.do_regroup_children()

            self.dispatch_event('on_regroup')

    @update_function
    def _repack_and_regroup_children(self):
        """
        Resize and regroup the children of this widget if this widget is 
        already attached to the GUI.  Otherwise, don't do anything.

        Container widgets should call this method whenever a new child widget 
        is attached.  

        Before a widget is attached to the GUI, it can't have a size or a group 
        because these attributes derive from a parent widget.  If any children 
        are attached to the widget at this point, they cannot be given sizes or 
        groups for the same reason.  Once the widget is attached to the GUI, it 
        will be given a size and a group by its parent, then it will give sizes 
        and groups to the children already attached to it.  If any children are 
        attached after this point, they should be given a size and group right 
        away.

        Note that what happens when a child widget is attached to its parent 
        depends on whether the parent is already attached to the GUI or not.  
        If it is, the child is resized and regrouped (other children may be 
        resized and regrouped at the same time).  Otherwise, nothing happens.  
        This method handles this logic.  As long as container subclasses call 
        this method each time a child is added or removed, their children will 
        be properly sized and grouped no matter when they were attached.
        """
        if self.is_attached_to_gui:
            self._repack()
            if self.__num_children > 0:
                self.do_regroup_children()

    def _attach_child(self, child):
        """
        Add a child to this widget.

        This method checks to make sure the child isn't already attached to 
        some other widget, tells the child who it's new parent is, and adds the 
        child to an internal list of children widgets.

        This method is only meant to be called in subclasses of Widget, which 
        is why it's prefixed with an underscore.  For example, you would use 
        this method if implementing a 
        """
        if not isinstance(child, Widget):
            raise UsageError(f"{child} is not a widget, did you forget to inherit from something?")

        if child.parent is self:
            return child

        if child.parent is not None:
            raise UsageError(f"{child} is already attached to {child.parent}, cannot attach to {self}")

        child.__parent = self
        self.__children.add(child)

        if self.is_attached_to_gui:
            for widget in child.__yield_self_and_all_children():
                widget.__root = self.root
                widget.do_attach()
                widget.dispatch_event('on_attach', widget)

        if self.is_hidden:
            self._hide_children()
        else:
            self._unhide_children(False)

        self.dispatch_event('on_attach_child', self, child)
        return child

    def _detach_child(self, child):
        """
        Detach a child from this widget.

        This method checks to make sure the child is currently attached to this 
        widget, undraws the child, resets several of the child's attributes 
        that might have been set by this widget, and removes the child from an 
        internal list of children widgets.
        
        This method is only meant to be called in subclasses of Widget, which 
        is why it's prefixed with an underscore.
        """
        if child.parent is not self:
            raise UsageError('{} is attached to {}, cannot detach from {}.'.format(child, child.parent, self))

        for widget in child.__yield_self_and_all_children():
            widget.do_detach()
            widget.dispatch_event('on_detach', widget)
            widget._ungrab_mouse()
            widget._undraw()
            widget.__root = None

        self.__children.discard(child)
        child.__parent = None

        # Set the detached child's ``group`` and ``rect`` attributes to None, 
        # so that its new parent will have to supply them before the child can 
        # be drawn again.
        child.__group = None
        child.__rect = None

        # Let the user react to a child being attached.
        self.dispatch_event('on_detach_child', self, child)

        # Derived classes are expected to call _repack_and_regroup_children() 
        # after this method.
        return child

    @update_function
    def _draw(self):
        """
        Create or update the vertex lists needed to render the widget.

        A widget should call this method whenever its appearance may have 
        changed (e.g. because an attribute was set or something) but its size 
        hasn't.  If its size may have changed, call `_repack()` instead.  The 
        actual drawing itself is delegated to `do_draw()`, so subclasses should 
        implement that method instead of overriding this one.  In order for a 
        widget to be drawn, four conditions need to be met:

        1. The widget must be connected to the root of the widget hierarchy.  
           Widgets get their pyglet batch object from the root widget, so 
           without this connection they cannot be drawn.

        2. The widget must have a size specified by its ``rect`` attribute.  
           This attribute is set when the widget is attached to the hierarchy 
           and its parent calls its `_resize()` method.

        3. The widget must be associated with a pyglet graphics group, which 
           controls things like how the widget will be stacked or scrolled.  A 
           group is set when the widget is attached to the hierarchy and its 
           parent calls its `_regroup()` method.

        4. The widget must not be hidden.
        """
        if self.root is None: return
        if self.rect is None: return
        if self.group is None: return
        if self.is_hidden: return

        self.do_draw()

    def _draw_all(self):
        """
        Draw this widget and all of its children.
        """
        self._draw()
        for child in self.__children:
            child._draw_all()

    def _undraw(self):
        """
        Delete the vertex lists being used to render this widget.

        This method is called when the widget is hidden or removed from the 
        GUI.  The actual work is delegated to `do_undraw()`, so subclasses 
        should implement that method rather than overriding this one.
        """
        self.do_undraw()

    def _undraw_all(self):
        """
        Undraw this widget and all of its children.
        """
        self._undraw()

        for child in self.__children:
            child._undraw_all()

    def _grab_mouse(self):
        """
        Force all mouse events to be funneled to this widget.

        The mouse events will still pass through all of this widget's parents.
        This is necessary because some of those parents may transform the 
        mouse coordinates, e.g. for scrolling.  However, the usual search for 
        the children under the mouse is short-circuited and the events are 
        always directed towards this widget.

        This method will fail with an exception if another widget is already 
        grabbing the mouse.
        """
        if self.is_root:
            return

        if self.parent.__mouse_grabber is not None:
            grabber = self.root.__find_mouse_grabber()
            raise UsageError(f"{grabber} is already grabbing the mouse, {self} can't grab it.")

        self.parent.__mouse_grabber = self
        self.parent._grab_mouse()

    def _ungrab_mouse(self, x=None, y=None):
        """
        Release the mouse and allow mouse events to be handled as usual again.

        If ``x`` and ``y`` coordinates are provided, an ``on_mouse_motion`` 
        event will be triggered by the root widget.  In some circumstances, 
        this is necessary to get widgets to re-evaluate their appearance (e.g.  
        rollover state) after the mouse is released.
        
        It is not an error to call this method if the widget is not actually 
        grabbing the mouse, it just won't do anything.
        """
        if not self.is_attached_to_gui:
            return

        if self.is_root:
            if (x, y) != (None, None):
                self.dispatch_event('on_mouse_motion', x, y, 0, 0)
            return

        if self.parent.__mouse_grabber is self:
            self.parent.__mouse_grabber = None
            self.parent._ungrab_mouse(x, y)

    def _hide_children(self):
        """
        Hide all of the widget's children.

        This method is part of the process of hiding the widget itself, so it 
        is assumed that the widget is hidden when this method is called.
        """
        for child in self.__yield_all_children():
            if child.is_visible:
                child._ungrab_mouse()
                child._undraw()

            child.__is_parent_hidden = True

    def _unhide_children(self, draw=True):
        """
        Redraw any of the widget's children that were visible before the widget 
        itself was hidden.

        This method is part of the process of unhiding the widget, so it is 
        assumed that the widget itself is already visible.  We can't simply 
        draw every child, because some of the children may have been explicitly 
        hidden independently of this one.
        """
        def unhide_child(child): #
            # Indicate that this child's parent is no longer hidden.
            child.__is_parent_hidden = False

            # If the child itself isn't hidden:
            if child.is_visible:

                # Draw the widget unless the caller asked not to.
                if draw:
                    child._draw()

                # Recursively unhide the child's children.
                for grandchild in child.__children:
                    unhide_child(grandchild)

        for child in self.__children:
            unhide_child(child)

    def __yield_all_children(self):
        """
        Recursively iterate over all of this widget's children and 
        grandchildren.
        """
        for child in self.__children:
            yield from child.__yield_self_and_all_children()

    def __yield_self_and_all_children(self):
        """
        Recursively iterate over this widget and all of its children and 
        grandchildren.
        """
        yield self
        yield from self.__yield_all_children()

    def __find_children_under_mouse(self, x, y):
        """
        Track and return the children that are under the given mouse 
        coordinate.

        If the mouse is being grabbed by a widget, then only that widget will 
        be returned.  Otherwise, all the children will be searched and the ones 
        that are under the mouse (and not hidden) will be returned.  The 
        returned data structure will also indicate which children were 
        previously under the mouse, which is important for propagating 
        `on_mouse_enter` and `on_mouse_leave` events.  

        You can change the boundaries to any particular widget class by 
        reimplementing the `is_under_mouse()` method.  Making widgets that seem 
        to be circular is a common reason to do this, for example.  You can 
        also change how the children widgets are searched by reimplementing the 
        `find_children_near_mouse()` method.  The `Grid` widget does this to 
        replace the default linear-time search with a constant-time one that 
        takes advantage of the grids predictable geometry.
        """
        previously_under_mouse = self.__children_under_mouse

        if self.__mouse_grabber is not None:
            self.__children_under_mouse = {self.__mouse_grabber}
        else:
            self.__children_under_mouse = {
                    w for w in self.do_find_children_near_mouse(x, y)
                    if w.is_visible and w.is_under_mouse(x, y)
            }

        return Widget.__ChildrenUnderMouse(
                previously_under_mouse, self.__children_under_mouse)

    def __find_children_under_mouse_after_leave(self):
        """
        Update the list of children under the mouse as the mouse leaves this 
        widget.

        You might think there would be no children under the mouse, if the 
        mouse is not even over the parent widget, but that isn't necessarily 
        true if the mouse is being grabbed.  Furthermore, it's important to 
        keep updating the list of children under the mouse while the mouse is 
        being grabbed, otherwise you can get weird artifacts when the mouse is 
        finally released.
        """
        previously_under_mouse = self.__children_under_mouse

        if self.__mouse_grabber is not None:
            self.__children_under_mouse = {self.__mouse_grabber}
        else:
            self.__children_under_mouse = set()

        return Widget.__ChildrenUnderMouse(
                previously_under_mouse, self.__children_under_mouse)

    def __find_mouse_grabber(self):
        """
        Return which of this widget's children or grandchildren is grabbing the 
        mouse, or None if none of them are.
        
        If this method returns a widget, all mouse events received by this 
        widget will be directed to that widget, regardless of where it is in 
        relation to the mouse.  Also note that unless this method is being 
        called on the root widget, some widget elsewhere in the hierarchy may 
        still be grabbing the mouse even if this returns None.
        """
        if self.__mouse_grabber is None:
            return None

        def recursive_find(parent):
            grabber = parent.__mouse_grabber
            return parent if grabber is None else recursive_find(grabber)

        return recursive_find(self)

    def __update_rollover_state(self, new_state, x, y):
        """
        Notify anyone who's interested that the mouse just interacted with this 
        widget.

        The rollover event contains the current rollover state and the previous 
        state.  This method helps keep track of those things, and also prevents 
        the widget from dispatching rollover events if nothing actually changed
        (i.e. you can't transition from "over" to "over").
        """
        self.__rollover_state = new_state
        
        # Check to see if the widget is actually under the mouse, and put it in 
        # the base state if it isn't.  This helps avoid visual artifacts when 
        # the mouse is ungrabbed.
        if not self.is_under_mouse(x, y):
            self.__rollover_state = 'base'

        if self.__rollover_state != self.__last_rollover_state:
            self.dispatch_event(
                    'on_rollover',
                    self, self.__rollover_state, self.__last_rollover_state,
            )
            self.__last_rollover_state = self.__rollover_state

    def __get_num_children(self):
        """
        Return the number of children attached to this widget.
        """
        return len(self.__children)
    def __set_padding(self, all=None, horz=None, vert=None,
            left=None, right=None, top=None, bottom=None):
        """
        Without repacking, set the four padding attributes (top, left, bottom, 
        right) defined in the Widget class.
        
        This method is provided to help the Widget constructor initialize its 
        default paddings without calling any polymorphic methods (which may in 
        turn depend on the constructor have been called).  Outside the 
        constructor this method should never be used; just use the regular 
        padding attributes instead.
        """
        self.__left_padding = first_not_none((left, horz, all, 0))
        self.__right_padding = first_not_none((right, horz, all, 0))
        self.__top_padding = first_not_none((top, vert, all, 0))
        self.__bottom_padding = first_not_none((bottom, vert, all, 0))

    size_hint = property(
            get_size_hint, lambda self, size: self.set_size_hint(*size))
    padding = late_binding_property(get_padding, set_padding)
    __num_children = property(__get_num_children)


    class __ChildrenUnderMouse:
        """
        A data structure that tracks which children widget are currently under 
        the mouse, and also which were under the mouse the last time the mouse 
        position was updated.

        With this information, we can work out which widgets the mouse entered 
        or exited since the last update, so we can send the appropriate events 
        to those widgets.
        """

        def __init__(self, previous, current):
            self.__previous = set(previous)
            self.__current = set(current)

        @property
        def previous(self):
            """
            The widgets previously under the mouse.
            """
            return self.__previous

        @property
        def current(self):
            """
            The widgets currently under the mouse.
            """
            return self.__current

        @property
        def entered(self):
            """
            The widgets that the mouse just began interacting with.
            """
            return self.current - self.previous

        @property
        def unchanged(self):
            """
            The widgets that the mouse has been interacting with for at least 
            two updates.
            """
            return self.current & self.previous

        @property
        def exited(self):
            """
            The widgets that the mouse just stopped interacting with.
            """
            return self.previous - self.current

