class MathApp(App):
    """
    MathApp is a subclass of the ggame :class:`~ggame.app.App` class. It
    incorporates the following extensions:

    * Support for zooming the display using the mouse wheel
    * Support for click-dragging the display using the mouse button
    * Automatic execution of step functions in all objects and sprites
        sub-classed from :class:`_MathDynamic`.

    :param float scale: Optional parameter sets the initial scale of the
        display in units of pixels per logical unit. The default is 200.

    :returns: MathApp instance
    """

    _DEFAULTSCALE = 200
    scale = _DEFAULTSCALE  # pixels per unit
    _xcenter = 0  # center of screen in units
    _ycenter = 0
    _mathVisualList = []
    _mathDynamicList = []
    _mathMovableList = []
    _mathSelectableList = []
    _mathStrokableList = []
    _viewNotificationList = []
    time = 0

    def __init__(self, scale=_DEFAULTSCALE):
        MathApp.time = 0
        self._starttime = time()
        super().__init__()
        MathApp.scale = scale  # pixels per unit
        # register event callbacks
        self.listenMouseEvent("click", self._handleMouseClick)
        self.listenMouseEvent("mousedown", self._handleMouseDown)
        self.listenMouseEvent("mouseup", self._handleMouseUp)
        self.listenMouseEvent("mousemove", self._handleMouseMove)
        self.listenMouseEvent("wheel", self._handleMouseWheel)
        self._mousedown = False
        self._mouse_captured_object = None
        self._mouse_stroked_object = None
        self._mouse_down_object = None
        self._mouse_x = self._mouse_y = None
        self._touchAllVisuals()
        self._selectedobj = None

    def step(self):
        """
        The step method overrides :func:`~ggame.app.App.step` in the
        :class:`~ggame.app.App` class, executing step functions in all
        objects subclassed from :class:`_MathDynamic`.
        """
        MathApp.time = time() - self._starttime
        for spr in self._mathDynamicList:
            spr.step()

    def _touchAllVisuals(self):
        # touch all visual object assets to use scaling
        for obj in self._mathVisualList:
            obj.touchAsset(True)

    @classmethod
    def logicalToPhysical(cls, lp):
        """
        Transform screen coordinates from logical to physical space. Output
        depends on the current 'zoom' and 'pan' of the screen.

        :param tuple(float,float) lp: Logical screen coordinates (x, y)

        :rtype: tuple(float,float)

        :returns: Physical screen coordinates (x, y)
        """

        def xxform(xvalue, xscale, xcenter, physwidth):
            """X transform"""
            return int((xvalue - xcenter) * xscale + physwidth / 2)

        def yxform(yvalue, yscale, ycenter, physheight):
            """Y transform"""
            return int(physheight / 2 - (yvalue - ycenter) * yscale)

        try:
            return (
                xxform(lp[0], cls.scale, cls._xcenter, cls.win.width),
                yxform(lp[1], cls.scale, cls._ycenter, cls.win.height),
            )
        except AttributeError:
            return lp

    @classmethod
    def physicalToLogical(cls, pp):
        """
        Transform screen coordinates from physical to logical space. Output
        depends on the current 'zoom' and 'pan' of the screen.

        :param tuple(float,float) lp: Physical screen coordinates (x, y)

        :rtype: tuple(float,float)

        :returns: Logical screen coordinates (x, y)
        """

        def xxform(xvalue, xscale, xcenter, physwidth):
            """X transform"""
            return (xvalue - physwidth / 2) / xscale + xcenter

        def yxform(yvalue, yscale, ycenter, physheight):
            """Y transform"""
            return (physheight / 2 - yvalue) / yscale + ycenter

        try:
            return (
                xxform(pp[0], cls.scale, cls._xcenter, cls.win.width),
                yxform(pp[1], cls.scale, cls._ycenter, cls.win.height),
            )
        except AttributeError:
            return pp

    @classmethod
    def translateLogicalToPhysical(cls, pp):
        """
        Transform screen translation from logical to physical space. Output
        only depends on the current 'zoom' of the screen.

        :param tuple(float,float) lp: Logical screen translation pair
            (delta x, delta y)

        :rtype: tuple(float,float)

        :returns: Physical screen translation ordered pair (delta x, delta y)
        """

        def xxform(xvalue, xscale):
            """X transform"""
            return xvalue * xscale

        def yxform(yvalue, yscale):
            """Y transform"""
            return -yvalue * yscale

        try:
            return (xxform(pp[0], cls.scale), yxform(pp[1], cls.scale))
        except AttributeError:
            return pp

    @classmethod
    def translatePhysicalToLogical(cls, pp):
        """
        Transform screen translation from physical to logical space. Output
        only depends on the current 'zoom' of the screen.

        :param tuple(float,float) lp: Physical screen translation pair
            (delta x, delta y)

        :rtype: tuple(float,float)

        :returns: Logical screen translation ordered pair (delta x, delta y)
        """

        def xxform(xvalue, xscale):
            """X transform"""
            return xvalue / xscale

        def yxform(yvalue, yscale):
            """Y transform"""
            return -yvalue / yscale

        try:
            return (xxform(pp[0], cls.scale), yxform(pp[1], cls.scale))
        except AttributeError:
            return pp

    def _handleMouseClick(self, event):
        found = False
        for obj in self._mathSelectableList:
            if obj.physicalPointTouching((event.x, event.y)):
                found = True
                if not obj.selected:
                    obj.select()
                    self._selectedobj = obj
        if not found and self._selectedobj:
            self._selectedobj.unselect()
            self._selectedobj = None

    def _handleMouseDown(self, event):
        self._mousedown = True
        self._mouse_captured_object = None
        self._mouse_stroked_object = None
        for obj in self._mathSelectableList:
            if obj.physicalPointTouching((event.x, event.y)):
                obj.mousedown()
                self._mouse_down_object = obj
                break
        for obj in self._mathMovableList:
            if obj.physicalPointTouching((event.x, event.y)) and not (
                obj.strokable and obj.canstroke((event.x, event.y))
            ):
                self._mouse_captured_object = obj
                break
        if not self._mouse_captured_object:
            for obj in self._mathStrokableList:
                if obj.canstroke((event.x, event.y)):
                    self._mouse_stroked_object = obj
                    break

    def _handleMouseUp(self, _event):
        if self._mouse_down_object:
            self._mouse_down_object.mouseup()
            self._mouse_down_object = None
        self._mousedown = False
        self._mouse_captured_object = None
        self._mouse_stroked_object = None

    def _handleMouseMove(self, event):
        if not self._mouse_x:
            self._mouse_x = event.x
            self._mouse_y = event.y
        dx = event.x - self._mouse_x
        dy = event.y - self._mouse_y
        self._mouse_x = event.x
        self._mouse_y = event.y
        if self._mousedown:
            if self._mouse_captured_object:
                self._mouse_captured_object.translate((dx, dy))
            elif self._mouse_stroked_object:
                self._mouse_stroked_object.stroke(
                    (self._mouse_x, self._mouse_y), (dx, dy)
                )
            else:
                lmove = self.translatePhysicalToLogical((dx, dy))
                MathApp._xcenter -= lmove[0]
                MathApp._ycenter -= lmove[1]
                self._touchAllVisuals()
                self._viewNotify("translate")

    def _handleMouseWheel(self, event):
        zoomfactor = event.wheeldelta / 100
        zoomfactor = 1 + zoomfactor if zoomfactor > 0 else 1 + zoomfactor
        if zoomfactor > 1.2:
            zoomfactor = 1.2
        elif zoomfactor < 0.8:
            zoomfactor = 0.8
        MathApp.scale *= zoomfactor
        self._touchAllVisuals()
        self._viewNotify("zoom")

    @property
    def view_position(self):
        """
        Attribute is used to get or set the current logical coordinates
        at the center of the screen as a tuple of floats (x,y).
        """
        return (MathApp._xcenter, MathApp._ycenter)

    @view_position.setter
    def view_position(self, pos):
        MathApp._xcenter, MathApp._ycenter = pos
        self._touchAllVisuals()
        self._viewNotify("translate")

    @classmethod
    def addViewNotification(cls, handler):
        """
        Register a function or method to be called in the event the view
        position or zoom changes.

        :param function handler: The function or method to be called
        :returns: Nothing
        """
        cls._viewNotificationList.append(handler)

    @classmethod
    def removeViewNotification(cls, handler):
        """
        Remove a function or method from the list of functions to be called
        in the event of a view position or zoom change.

        :param function handler: The function or method to be removed
        :returns: Nothing
        """
        cls._viewNotificationList.remove(handler)

    def _viewNotify(self, viewchange):
        for handler in self._viewNotificationList:
            handler(
                viewchange=viewchange,
                scale=self.scale,
                center=(self._xcenter, self._ycenter),
            )

    @classmethod
    def distance(cls, pos1, pos2):
        """
        Utility for calculating the distance between any two points.

        :param tuple(float,float) pos1: The first point
        :param tuple(float,float) pos2: The second point
        :rtype: float
        :returns: The distance between the two points (using Pythagoras)
        """
        return sqrt((pos2[0] - pos1[0]) ** 2 + (pos2[1] - pos1[1]) ** 2)

    @classmethod
    def addVisual(cls, obj):
        """
        Add a visual object to the visual object list.

        :param object obj: The object to add
        :returns: None
        """
        if isinstance(obj, _MathVisual):
            cls._mathVisualList.append(obj)

    @classmethod
    def removeVisual(cls, obj):
        """
        Remove a visual object from the visual object list.

        :param object obj: The object to remove
        :returns: None
        """
        if isinstance(obj, _MathVisual) and obj in cls._mathVisualList:
            cls._mathVisualList.remove(obj)

    @classmethod
    def addDynamic(cls, obj):
        """
        Add a dynamic object to the dynamic object list.

        :param object obj: The object to add
        :returns: None
        """
        if isinstance(obj, _MathDynamic) and obj not in cls._mathDynamicList:
            cls._mathDynamicList.append(obj)

    @classmethod
    def removeDynamic(cls, obj):
        """
        Remove a dynamic object from the dynamic object list.

        :param object obj: The object to remove
        :returns: None
        """
        if isinstance(obj, _MathDynamic) and obj in cls._mathDynamicList:
            cls._mathDynamicList.remove(obj)

    @classmethod
    def addMovable(cls, obj):
        """
        Add a movable object to the movable object list.

        :param object obj: The object to add
        :returns: None
        """
        if isinstance(obj, _MathVisual) and obj not in cls._mathMovableList:
            cls._mathMovableList.append(obj)

    @classmethod
    def removeMovable(cls, obj):
        """
        Remove a movable object from the movable object list.

        :param object obj: The object to remove
        :returns: None
        """
        if isinstance(obj, _MathVisual) and obj in cls._mathMovableList:
            cls._mathMovableList.remove(obj)

    @classmethod
    def addSelectable(cls, obj):
        """
        Add a selectable object to the selectable object list.

        :param object obj: The object to add
        :returns: None
        """
        if isinstance(obj, _MathVisual) and obj not in cls._mathSelectableList:
            cls._mathSelectableList.append(obj)

    @classmethod
    def removeSelectable(cls, obj):
        """
        Remove a selectable object from the selectable object list.

        :param object obj: The object to remove
        :returns: None
        """
        if isinstance(obj, _MathVisual) and obj in cls._mathSelectableList:
            cls._mathSelectableList.remove(obj)

    @classmethod
    def addStrokable(cls, obj):
        """
        Add a strokable object to the strokable object list.

        :param object obj: The object to add
        :returns: None
        """
        if isinstance(obj, _MathVisual) and obj not in cls._mathStrokableList:
            cls._mathStrokableList.append(obj)

    @classmethod
    def removeStrokable(cls, obj):
        """
        Remove a strokable object from the strokable object list.

        :param object obj: The object to remove
        :returns: None
        """
        if isinstance(obj, _MathVisual) and obj in cls._mathStrokableList:
            cls._mathStrokableList.remove(obj)

    @classmethod
    def destroy(cls):
        """
        This will clean up any class level storage.
        """
        App.destroy()  # hit the App class first
        MathApp.time = 0
        MathApp._mathVisualList = []
        MathApp._mathDynamicList = []
        MathApp._mathMovableList = []
        MathApp._mathSelectableList = []
        MathApp._mathStrokableList = []
        MathApp._viewNotificationList = [] class Slider(_MathVisual):
    """
    Create a 'slider' style numeric input control on the screen. This is a
    subclass of :class:`~ggame.sprite.Sprite` and
    :class:`~ggame.mathapp._MathVisual` but most of the inherited
    members are of little use and are not shown in the documentation.


    :param \\*args:
        See below
    :param \\**kwargs:
        See below

    :Required Arguments:
        * **pos**  (*tuple(float,float)*) Screen position of the slider, which may
            be a literal tuple of floats, or a reference to any object or
            function that returns or evaluates to a tuple of floats.
        * **minval** (*float*) The minimum value of the slider
        * **maxval** (*float*) The maximum value of the slider
        * **initial** (*float*) The initial value of the slider


    :Optional Keyword Arguments:
        * **steps** (*int*) Number of steps between minval and maxval (default 50)
        * **leftkey** (*str*) Name of a keyboard key that will make the
            slider control step down (move left) (default None). See
            :class:`~ggame.event.KeyEvent` for a list of names.
        * **rightkey** (*str*) Name of a keyboard key that will make the slider
            control step up (move right) (default None)
        * **centerkey** (*str*) Name of a keyboard key that will make the slider
            move to its center position (default None)
        * **positioning** (*str*) One of 'logical' or 'physical'
        * **size** (*int*) Width of the slider (in pixels)
        * **color** (*Color*) Valid :class:`~ggame.asset.Color` object
        * **style** (*LineStyle*) Valid :class:`~ggame.asset.LineStyle` object

    Example:

    .. literalinclude:: ../examples/sliderslider.py

    """

    _posinputsdef = ["pos"]
    _nonposinputsdef = ["minval", "maxval", "initial"]

    def __init__(self, *args, **kwargs):
        super().__init__(RectangleAsset(1, 1), *args, **kwargs)
        self._val = self._nposinputs.initial()  # pylint: disable=no-member
        self._steps = kwargs.get("steps", 50)
        self._step = (
            self._nposinputs.maxval()  # pylint: disable=no-member
            - self._nposinputs.minval()  # pylint: disable=no-member
        ) / self._steps
        self._leftctrl = kwargs.get("leftkey", None)
        self._rightctrl = kwargs.get("rightkey", None)
        self._centerctrl = kwargs.get("centerkey", None)
        self.selectable = True  # must be after super init!
        self.strokable = True  # this enables grabbing/slideing the thumb
        self.thumbcaptured = False
        self._thumbwidth = max(self._stdinputs.width() / 40, 1)
        self._thumb = Sprite(
            RectangleAsset(
                self._thumbwidth,
                self._stdinputs.size() - 2,
                LineStyle(1, self._stdinputs.color()),
                self._stdinputs.color(),
            ),
            self._thumbXY(),
        )
        self.touchAsset()
        if self._leftctrl:
            MathApp.listenKeyEvent("keydown", self._leftctrl, self._moveLeft)
        if self._rightctrl:
            MathApp.listenKeyEvent("keydown", self._rightctrl, self._moveRight)
        if self._centerctrl:
            MathApp.listenKeyEvent("keydown", self._centerctrl, self._moveCenter)

    def _thumbXY(self):
        minval = self._nposinputs.minval()  # pylint: disable=no-member
        maxval = self._nposinputs.maxval()  # pylint: disable=no-member
        return (
            self._spposinputs.pos[0]
            + (self._val - minval)
            * (self._sstdinputs.width - self._thumbwidth)
            / (maxval - minval),
            self._spposinputs.pos[1] + 1,
        )

    def __call__(self):
        return self._val

    @property
    def value(self):
        """
        Report value of the slider. Attribute is get-able and set-able.
        """
        return self._val

    @value.setter
    def value(self, val):
        self._setval(val)

    def _buildAsset(self):
        self._setThumb()
        return RectangleAsset(
            self._stdinputs.width(),
            self._stdinputs.size(),
            line=self._stdinputs.style(),
            fill=Color(0, 0),
        )

    def _setThumb(self):
        self._thumb.position = self._thumbXY()

    def step(self):
        pass

    def _setval(self, val):
        minval = self._nposinputs.minval()  # pylint: disable=no-member
        maxval = self._nposinputs.maxval()  # pylint: disable=no-member
        if val <= minval:
            self._val = minval
        elif val >= maxval:
            self._val = maxval
        else:
            self._val = (
                round((val - minval) * self._steps / (maxval - minval)) * self._step
                + minval
            )
        self._setThumb()

    def increment(self, step):
        """
        Increment the slider value.

        :param float step: The amount by which the slider control should be adjusted.
        :returns: None
        """

        self._setval(self._val + step)

    def select(self):
        super().select()
        if not self._leftctrl:
            MathApp.listenKeyEvent("keydown", "left arrow", self._moveLeft)
        if not self._rightctrl:
            MathApp.listenKeyEvent("keydown", "right arrow", self._moveRight)
        MathApp.listenMouseEvent("click", self._mouseClick)

    def unselect(self):
        super().unselect()
        try:
            if not self._leftctrl:
                MathApp.unlistenKeyEvent("keydown", "left arrow", self._moveLeft)
            if not self._rightctrl:
                MathApp.unlistenKeyEvent("keydown", "right arrow", self._moveRight)
            MathApp.unlistenMouseEvent("click", self._mouseClick)
        except ValueError:
            pass

    def _mouseClick(self, event):
        if self.physicalPointTouching((event.x, event.y)):
            if event.x > self._thumb.x + self._thumbwidth:
                self._moveRight(event)
            elif event.x < self._thumb.x:
                self._moveLeft(event)

    def _moveLeft(self, _event):
        self.increment(-self._step)

    def _moveRight(self, _event):
        self.increment(self._step)

    def _moveCenter(self, _event):
        self._val = (self._snposinputs.minval + self._snposinputs.maxval) / 2
        self._setThumb()

    def canstroke(self, ppos):
        """
        Function returns true if the given physical position corresponds with a part
        of the slider that can be dragged (i.e. the thumb).

        :param (float,float) ppos: Physical screen coordinates.
        :return: True if the position represents a draggable part of the control.
        :rtype: boolean

        """
        return self.physicalPointTouchingThumb(ppos)

    def stroke(self, ppos, pdisp):
        """
        Function performs the action of stroking or click-dragging on the slider
        control (i.e. dragging the thumb).

        :param (float, float) ppos: Physical screen coordinates.
        :param (float, float) pdisp: Physical displacement vector.
        :return: None

        """
        _ppos = self._spposinputs.pos
        minval = self._snposinputs.minval
        maxval = self._snposinputs.maxval
        xpos = ppos[0] + pdisp[0]
        self.value = (xpos - _ppos[0]) * (
            maxval - minval
        ) / self._sstdinputs.width + minval

    def physicalPointTouching(self, ppos):
        _ppos = self._spposinputs.pos
        return (
            ppos[0] >= _ppos[0]
            and ppos[0] <= _ppos[0] + self._sstdinputs.width
            and ppos[1] >= _ppos[1]
            and ppos[1] <= _ppos[1] + self._sstdinputs.size
        )

    def physicalPointTouchingThumb(self, ppos):
        """
        Determine if a physical screen location is touching the slider "thumb".

        :param (float, float) ppos: Physical screen coordinates.
        :returns: True if touching, False otherwise.
        :rtype: boolean
        """

        thumbpos = self._thumbXY()
        return (
            ppos[0] >= thumbpos[0]
            and ppos[0] <= thumbpos[0] + self._thumbwidth + 2
            and ppos[1] >= thumbpos[1]
            and ppos[1] <= thumbpos[1] + self._sstdinputs.size - 2
        )

    def translate(self, pdisp):
        pass

    def destroy(self):
        self._thumb.destroy()
        super().destroy() class InputImageButton(ImagePoint):
    """
    :class:`~ggame.point.ImagePoint` object that uses an image as its on-screen and
        activates a callback function when pressed.

    :Required Arguments:

    :param str url: Location of an image file (png, jpg)
    :param function callback: Reference to a function to execute, passing this
        button object.

    :param \\*args:
        See below
    :param \\**kwargs:
        See below

    :Required Arguments:
        * **pos** (*tuple(float,float)*) Position in physical or logical units.

    :Optional Keyword Arguments:
        * **positioning** (*str*) One of 'logical' (default) or 'physical'
        * **frame** (*Frame*) The sub-frame location of image within the image file
        * **qty** (*int*) The number of sub-frames, when used as a sprite sheet
        * **direction** (*str*) One of 'horizontal' (default) or 'vertical'
        * **margin** (*int*) Pixels between sub-frames if sprite sheet

    Example:

    .. literalinclude:: ../examples/inputpointinputimagebutton.py

    """

    def __init__(self, url, callback, *args, **kwargs):
        super().__init__(url, *args, **kwargs)
        self.center = (0, 0)
        self._callback = callback
        self.selectable = True
        self.firstImage()
        self.mousewasdown = self.mouseisdown

    def select(self):
        super().select()
        if self._callback:
            self._callback(self)
        self.unselect()

    def __call__(self):
        # code for controlling the button image state only works if the
        # button state is being monitored!
        if self.mouseisdown != self.mousewasdown:
            if self.mouseisdown:
                self.nextImage()
            else:
                self.firstImage()
            self.mousewasdown = self.mouseisdown
        return self.mouseisdown class Label(_MathVisual):
    """
    Create a text label on the screen. This is a
    subclass of :class:`~ggame.sprite.Sprite` and
    :class:`~ggame.mathapp._MathVisual` but most of the inherited
    members are of little use and are not shown in the documentation.

    :param \\*args:
        See below
    :param \\**kwargs:
        See below

    :Required Arguments:
        * **pos**  (*tuple(float,float)*) Screen position of the label, which may
            be a literal tuple of floats, or a reference to any object or
            function that returns or evaluates to a tuple of floats.
        * **text** (*str*) Text to appear in the label. This may be a literal
            string or a reference to any object or function that returns or
            evaluates to a string.

    :Optional Keyword Arguments:
        * **positioning** (*str*) One of 'logical' or 'physical'
        * **size** (*int*) Size of text font (in pixels)
        * **width** (*int*) Width of the label (in pixels)
        * **color** (*Color*) Valid :class:`~ggame.asset.Color` object

    Example:

    .. literalinclude:: ../examples/labellabel.py

    """

    _posinputsdef = ["pos"]
    _nonposinputsdef = ["text"]

    def __init__(self, *args, **kwargs):
        """
        Required Inputs

        * **pos** position of label
        * **text** text contents of label
        """
        super().__init__(TextAsset(""), *args, **kwargs)
        self.touchAsset()

    def _buildAsset(self):
        return TextAsset(
            self._nposinputs.text(),  # pylint: disable=no-member
            style="{0}px Courier".format(self._stdinputs.size()),
            width=self._stdinputs.width(),
            fill=self._stdinputs.color(),
        )

    def __call__(self):
        return self._nposinputs.text()  # pylint: disable=no-member

    def physicalPointTouching(self, ppos):
        _ppos = self._spposinputs.pos
        return (
            ppos[0] >= _ppos[0]
            and ppos[0] <= _ppos[0] + self._sstdinputs.width
            and ppos[1] >= _ppos[1]
            and ppos[1] <= _ppos[1] + self._sstdinputs.size
        )

    def translate(self, pdisp):
        pass class InputButton(Label):
    """
    Create a :class:`~ggame.label.Label` that can be clicked with a mouse
    to execute a user-defined function. This is a
    subclass of :class:`~ggame.sprite.Sprite` and
    :class:`~ggame.mathapp._MathVisual` but most of the inherited
    members are of little use and are not shown in the documentation.

    :param function callback: A reference to a function to execute, passing
        this button object, when the button is clicked
    :param tuple(float,float) pos: Screen position of the control, which may
            be a literal tuple of floats, or a reference to any object or
            function that returns or evaluates to a tuple of floats.

    :param float val: Initial value of the input text

    :param \\*args:
        See below
    :param \\**kwargs:
        See below

    :Required Arguments:
        * **pos**  (*tuple(float,float)*) Screen position of the button, which may
            be a literal tuple of floats, or a reference to any object or
            function that returns or evaluates to a tuple of floats.
        * **text** (*str*) Text to appear in the button. This may be a literal
            string or a reference to any object or function that returns or
            evaluates to a string.


    :Optional Keyword Arguments:
        * **positioning** (*str*) One of 'logical' or 'physical'
        * **size** (*int*) Size of text font (in pixels)
        * **width** (*int*) Width of the label (in pixels)
        * **color** (*Color*) Valid :class:`~ggame.asset.Color` object

    Example:

    .. literalinclude:: ../examples/inputinputbutton.py

    """

    def __init__(self, callback, *args, **kwargs):
        """
        Required Inputs

        * **pos** position of button
        * **text** text of button
        * **callback** reference of a function to execute, passing this button object
        """
        super().__init__(*args, **kwargs)
        self.touchAsset()
        self._callback = callback
        self.selectable = True

    def _buildAsset(self):
        return TextAsset(
            self._nposinputs.text(),  # pylint: disable=no-member
            style="bold {0}px Courier".format(self._stdinputs.size()),
            width=self._stdinputs.width(),
            fill=self._stdinputs.color(),
        )

    def select(self):
        super().select()
        if self._callback:
            self._callback(self)
        self.unselect() class InputNumeric(Label):
    """
    Create a :class:`~ggame.label.Label` that can be selected to accept
    new **numeric** input from the keyboard. This is a
    subclass of :class:`~ggame.sprite.Sprite` and
    :class:`~ggame.mathapp._MathVisual` but most of the inherited
    members are of little use and are not shown in the documentation.

    :param tuple(float,float) pos: Screen position of the control, which may
            be a literal tuple of floats, or a reference to any object or
            function that returns or evaluates to a tuple of floats.

    :param float val: Initial value of the input text

    :param \\**kwargs:
        See below

    :Optional Keyword Arguments:
        * **fmt** (*str*) a Python format string (default is {0:.2f})
        * **positioning** (*str*) One of 'logical' or 'physical'
        * **size** (*int*) Size of text font (in pixels)
        * **width** (*int*) Width of the label (in pixels)
        * **color** (*Color*) Valid :class:`~ggame.asset.Color` object

    Example:

    .. literalinclude:: ../examples/inputinputnumeric.py

    """

    def __init__(self, pos, val, **kwargs):
        self._fmt = kwargs.get("fmt", "{0:.2f}")
        self._val = self.eval(val)()  # initialize to simple numeric
        self._savedval = self._val
        self._text = ""
        self._updateText()
        super().__init__(pos, self._textValue, **kwargs)
        self.selectable = True

    def _textValue(self):
        return self._text()

    def _updateText(self, editing=False):
        if editing:
            self._text = self.eval("{0}".format(self._val))
        else:
            self._text = self.eval(self._fmt.format(self._val))

    def processEvent(self, event):
        if event.key in "0123456789insertdelete":
            key = event.key
            if event.key == "insert":
                key = "-"
            elif event.key == "delete":
                key = "."
            if self._text() == "0":
                self._text = self.eval("")
            self._text = self.eval(self._text() + key)
            self.touchAsset()
        elif event.key in ["enter", "escape"]:
            if event.key == "enter":
                try:
                    self._val = float(self._text())
                except ValueError:
                    self._val = self._savedval
                self._savedval = self._val
            self.unselect()

    def select(self):
        super().select()
        self._savedval = self._val
        self._val = 0
        self._updateText(True)
        self.touchAsset()
        MathApp.listenKeyEvent("keypress", "*", self.processEvent)

    def unselect(self):
        super().unselect()
        self._val = self._savedval
        self._updateText()
        self.touchAsset()
        try:
            MathApp.unlistenKeyEvent("keypress", "*", self.processEvent)
        except ValueError:
            pass

    def __call__(self):
        return self._val class ImageIndicator(_MathVisual):
    """
    Use a sprite sheet image to indicate integer or boolean values.

    Required Inputs

    :param str url: Location of image file consisting of a multi-image sprite sheet.
    :param (float,float) pos: Position in physical or logical units. May be a
        :class:`ggame.point.Point` instance, a literal (x,y) pair, or a function
        that returns an (x,y) pair.
    :param int value: The state of the indicator. May be a function that returns
        a suitable (integer) value.

    :param \\**kwargs:
        See below


    :Optional Keyword Arguments:

    * **positioning** (*str*) One of 'logical' (default) or 'physical'
    * **frame** (*:class:`ggame.asset.Frame`*) Sub-frame location of sub-image within
        the main image.
    * **qty** (*int*) The number of sub-frames, when used as sprite sheet.
    * **direction** (*str*) One of 'horizontal' (default) or 'vertical'.
    * **margin** (*int*) Number of pixels between sub-frames if sprite sheet.

    Example:

    .. literalinclude:: ../examples/indicatorimageindicator.py


    """

    _posinputsdef = ["pos"]
    _nonposinputsdef = ["value"]

    def __init__(self, url, *args, **kwargs):
        kwargs.setdefault("frame", None)
        kwargs.setdefault("qty", 1)
        kwargs.setdefault("direction", "horizontal")
        kwargs.setdefault("margin", 0)
        super().__init__(
            ImageAsset(
                url,
                kwargs["frame"],
                kwargs["qty"],
                kwargs["direction"],
                kwargs["margin"],
            ),
            *args,
            **kwargs
        )
        self.center = (0, 0)

    def _buildAsset(self):
        # pylint: disable=no-member
        inval = self._nposinputs.value()
        # pylint: enable=no-member
        if inval:
            self.setImage(1)
        elif not inval:
            self.setImage(0)
        else:
            self.setImage(inval)
        return self.asset

    def physicalPointTouching(self, ppos):
        self.setExtents()  # ensure xmin, xmax are correct
        x, y = ppos
        return self.xmin <= x <= self.xmax and self.ymin <= y <= self.ymax

    def translate(self, pdisp):
        pass class ImageAsset(_Asset):
    """
    The ImageAsset class connects ggame to a specific image **file**.

    :param str url: All ImageAsset instances must specify a file name or
        url where a jpg or png image is located.

    :param Frame frame: If the desired sprite image exists in only a smaller
        sub-section of the original image, then specify an area `within` the
        image using `frame` parameter, which must be a valid :class:`Frame`
        instance.

    :param int qty=0: If the image file actually is a *collection* of images,
        such as a so-called *sprite sheet*, then the ImageAsset class supports
        defining a list of images, provided they exist in the original image
        as a **row** of evenly spaced images or a **column** of images. To
        specify this, provide the `qty` (quantity) of images in the row or
        column.

    :param str direction='horizontal': For an image *sprite sheet*, specify
        whether the images are oriented in a `'vertical'` or `'horizontal'`
        arrangement.

    :param int margin=0: If there is a gap between successive images in an image
        *sprite sheet*, then specify the size of the gap (in pixels). When
        used in this way, the `frame` parameter must define the
        area of only the **first** image in the collection; all subsequent
        images in the list are assumed to be the same size, but separated by the
        `margin` value.

    Example:

    .. literalinclude:: ../examples/assetimage.py
    """

    def __init__(self, url, frame=None, qty=1, direction="horizontal", margin=0):
        super().__init__()
        self.url = url
        """
        A string that represents the path or url of the original file.
        """
        del self.gfxlist[0]
        self.width = self.height = 0
        self.append(url, frame, qty, direction, margin)

    def append(self, url, frame=None, qty=1, direction="horizontal", margin=0):
        """
        Append a texture asset from a new image file (or url). This method
        allows you to build a collection of images into an asset (such as you
        might need for an animated sprite), but without using a single
        sprite sheet image.

        The parameters for the `append` method are identical to those
        supplied to the :class:`ImageAsset` initialization method.

        This method allows you to build up an asset that consists of
        multiple rows or columns of images in a sprite sheet or sheets.
        """
        gfx = GFX_Texture_fromImage(url, False)
        dx = 0
        dy = 0
        for i in range(qty):
            if frame is not None:
                self.width = frame.w
                self.height = frame.h
                if direction == "horizontal":
                    dx = frame.w + margin
                elif direction == "vertical":
                    dy = frame.h + margin
                f = Frame(frame.x + dx * i, frame.y + dy * i, frame.w, frame.h)
                gfx = GFX_Texture(gfx, f.gfx)
            else:
                self.width = gfx.width
                self.height = gfx.height
            self.gfxlist.append(gfx) class Frame(object):  # pylint: disable=useless-object-inheritance
    """
    Frame is a utility class for expressing the idea of a rectangular region.

    Initializing parameters describe the position of the upper-left corner
    of the frame, and the frame's width and height. The units are *typically*
    in pixels, though a frame can be generic.

    :param int x: X-coordinate of frame upper-left corner
    :param int y: Y-coordinate of frame upper-left corner
    :param int w: Width of the frame
    :param int h: Height of the frame
    """

    def __init__(self, x, y, w, h):

        self.gfx = GFX_Rectangle(x, y, w, h)
        """
        `_gfx` is a reference to the underlying object provided by the system.
        """
        self.x = x
        self.y = y
        self.w = w
        self.h = h

    @property
    def x(self):
        """
        X-coordinate of the upper left hand corner of this frame.
        """
        return self.gfx.x

    @x.setter
    def x(self, value):
        self.gfx.x = value

    @property
    def y(self):
        """
        Y-coordinate of the upper left hand corner of this frame.
        """
        return self.gfx.y

    @y.setter
    def y(self, value):
        self.gfx.y = value

    @property
    def w(self):
        """
        Width of the frame.
        """
        return self.gfx.width

    @w.setter
    def w(self, value):
        self.gfx.width = value

    @property
    def h(self):
        """
        Height of the frame.
        """
        return self.gfx.height

    @h.setter
    def h(self, value):
        self.gfx.height = value

    @property
    def center(self):
        """
        The `center` property computes a coordinate pair (tuple) for the
        center of the frame.

        The `center` property, when set, redefines the `x` and `y` properties
        of the frame in order to make the center agree with the coordinates
        (tuple) assigned to it.
        """

        return (self.x + self.w // 2, self.y + self.h // 2)

    @center.setter
    def center(self, value):
        c = self.center
        self.x += value[0] - c[0]
        self.y += value[1] - c[1] class Color:
    """
    The Color class is used to represent colors and/or colors with
    transparency.

    :param int color: an integer in the conventional format (usually
        as a hexadecimal literal, e.g. 0xffbb33 that represents
        the three color components, red, green and blue)

    :param float alpha: a transparency value, or `alpha` as a floating-point
        number in the range of 0.0 to 1.0 where 0.0 represents
        completely transparent and 1.0 represents completely opaque.

    Example:

    .. literalinclude:: ../examples/assetcolor.py
    """

    _colornames = {0xFFFFFF: "WHITE", 0x000000: "BLACK"}

    def __init__(self, color, alpha):
        self.color = color
        self.alpha = alpha
        self.name = "Color(0x{0:06X}, {1})".format(int(self.color), self.alpha)
        if alpha == 1.0:
            self.name = self._colornames.get(self.color, self.name)

    def __eq__(self, other):
        return (
            isinstance(self, type(other))
            and self.color == other.color
            and self.alpha == other.alpha
        )

    def __repr__(self):
        return self.name class LineStyle:
    """
    The LineStyle class is used to represent line style when
    drawing geometrical objects such as rectangles, ellipses, etc.

    :param int width: the `width` of the line in pixels

    :param Color color: the `color` as a valid :class:`Color` instance.

    Example:

    .. literalinclude:: ../examples/assetlinestyle.py

    This defines a 3-pixel wide green line.
    """

    def __init__(self, width, color):
        """
        """
        self.width = width
        self.color = color

    def __eq__(self, other):
        return (
            isinstance(self, type(other))
            and self.width == other.width
            and self.color == other.color
        )

    def __repr__(self):
        return "LineStyle({}, {})".format(self.width, self.color) class GlassButton(InputImageButton):
    """
    Glass button input control that triggers a callback when clicked.

    :Required Inputs:

    :param function callback: Reference to a function to execute, passing this button
        object as an argument.
    :param (float,float) pos: Position in physical or logical units. May be a
        :class:`ggame.point.Point` instance, a literal (x,y) pair, or a function
        that returns an (x,y) pair.

    Example:

    .. literalinclude:: ../examples/inputpointglassbutton.py

    """

    def __init__(self, callback, *args, **kwargs):
        kwargs.setdefault("frame", Frame(0, 0, 100, 100))
        kwargs.setdefault("qty", 2)
        super().__init__(
            self.getImagePath("button-round.png"), callback, *args, **kwargs
        )
        self.scale = 0.3 class MetalToggle(InputImageToggle):
    """
    Metal toggle input control that toggles through two states with each
    mouseclick.

    :Required Inputs:

    :param int initindex: Index to initial toggle state (0 or 1).
    :param (float,float) pos: Position in physical or logical units. May be a
        :class:`ggame.point.Point` instance, a literal (x,y) pair, or a function
        that returns an (x,y) pair.

    Example:

    .. literalinclude:: ../examples/indicatorledindicator.py

    """

    def __init__(self, initindex, *args, **kwargs):
        kwargs.setdefault("frame", Frame(0, 0, 110, 150))
        super().__init__(
            self.getImagePath("toggle-up-down.png"),
            [True, False],
            initindex,
            *args,
            **kwargs
        )
        self.scale = 0.4 class LEDIndicator(ImageIndicator):
    """
    Subclass of ImageIndicator using a red LED on/off image.

    Required Inputs

    :param (float,float) pos: Position in physical or logical units. May be a
        :class:`ggame.point.Point` instance, a literal (x,y) pair, or a function
        that returns an (x,y) pair.
    :param int value: The state of the indicator. May be a function that returns
        a suitable (integer) value.

    :param \\**kwargs:
        See below

    :Optional Keyword Arguments:

        * **positioning** (*str*) One of 'logical' (default) or 'physical'

    Example:

    .. literalinclude:: ../examples/indicatorledindicator.py

    """

    def __init__(self, *args, **kwargs):
        kwargs.setdefault("frame", Frame(0, 0, 600, 600))
        kwargs.setdefault("qty", 2)
        super().__init__(self.getImagePath("red-led-off-on.png"), *args, **kwargs)
        self.scale = 0.05 class Timer(_MathDynamic):
    """
    The Timer class instantiates an object whose basic function is to report
    the number of seconds since its creation by calling the object as a
    function with an empty argument list.

    The Timer class accepts no arguments during creation.

    Example of use:

    .. literalinclude:: ../examples/timertimer.py

    """

    def __init__(self):
        super().__init__()
        self._once = []
        self._callbacks = {}
        self._time = 0
        self.reset()
        MathApp.addDynamic(self)  # always dynamically defined

    def reset(self):
        """
        Set the reference time to the MathApp current time. If the timer is reset
        before the app initializes then do nothing.

        :returns: None
        """
        self._reset = MathApp.time

    @property
    def time(self):
        """
        Attribute is always updated with the number of seconds since the
        timer was created.
        """
        return self._time

    @time.setter
    def time(self, value):
        pass

    def step(self):
        nexttimers = []
        calllist = []
        self._time = MathApp.time - self._reset
        while self._once and self._once[0][0] <= MathApp.time:
            tickinfo = self._once.pop(0)
            if tickinfo[1]:  # periodic?
                nexttimers.append(
                    (tickinfo[1], self._callbacks[tickinfo][0])
                )  # delay, callback
            calllist.append(
                self._callbacks[tickinfo].pop(0)
            )  # remove callback and queue it
            if not self._callbacks[tickinfo]:  # if the callback list is empty
                del self._callbacks[tickinfo]  # remove the dictionary entry altogether
        for tickadd in nexttimers:
            self.callAfter(tickadd[0], tickadd[1], True)  # keep it going
        for call in calllist:
            call(self)

    def callAfter(self, delay, callback, periodic=False):
        """
        Set a callback to occur either once or periodically. The callback
        function should accept a single parameter which will be a reference
        to the timer object that called it.

        :param float delay: The number of seconds to wait before executing
            the callback function
        :param function callback: The callback function to call on timer
            expiration
        :param boolean periodic: Set True if the callback function should
            be executed periodically
        :returns: None
        """
        key = (self._time + delay, delay if periodic else 0)
        self._once.append(key)
        callbacklist = self._callbacks.get(key, [])
        callbacklist.append(callback)
        self._callbacks[key] = callbacklist
        self._once.sort()

    def callAt(self, calltime, callback):
        """
        Set a callback to occur at a specific time (seconds since
        the Timer object was created or since its
        :func:`~ggame.timer.Timer.reset` method was called.

        :param float time: The time to wait since timer creation or reset
            before calling the callback function.
        :param function callback: The callback function to call
        :returns: None
        """
        self.callAfter(calltime - self._time, callback)

    def callEvery(self, period, callback):
        """
        Set a callback to occur periodically. The callback
        function should accept a single parameter which will be a reference
        to the timer object that called it.

        :param float period: The number of seconds to wait before each
            execution of the callback function
        :param function callback: The callback function to call on timer
            expiration
        :returns: None
        """
        self.callAfter(period, callback, True)

    def __call__(self):
        return self._time
import unittest
import time
from ggame.point import Point
from ggame.circle import Circle
from ggame.mathapp import MathApp
from ggame.slider import Slider
from ggame.inputpoint import InputImageButton
from ggame.label import Label
from ggame.input import InputButton, InputNumeric
from ggame.point import ImagePoint
from ggame.line import LineSegment
from ggame.indicator import ImageIndicator
from ggame import Color, LineStyle, ImageAsset, Frame
from ggame.inputpoint import GlassButton, MetalToggle
from ggame.indicator import LEDIndicator
from ggame.timer import Timer


class TestMathMethods(unittest.TestCase):
    def buttonstatus(self):
        return "True" if self.imgbutton() else "False"

    def labelcoords(self):
        return (100 + self.vslider1(), 175)

    def buttoncoords(self):
        return (300 + self.vslider1(), 175)

    def labelcolor(self):
        colorval = self.vslider1()
        return Color(colorval * 256, 1)

    def pressbutton(self, caller):
        print("button pressed: ", caller)

    def __init__(self, arg):
        super().__init__(arg)

    def test_controls(self):
        self.imgbutton = InputImageButton(
            "images/button-round.png",
            self.pressbutton,
            (0, 0),
            frame=Frame(0, 0, 100, 100),
            qty=2,
        )
        self.imgbutton.scale = 0.5
        self.vslider1 = Slider(
            (100, 150), 0, 250, 125, positioning="physical", steps=10
        )
        self.label = Label(
            self.labelcoords,
            self.buttonstatus,
            size=15,
            positioning="physical",
            color=self.labelcolor,
        )
        self.button = InputButton(
            self.pressbutton,
            self.buttoncoords,
            "Press Me",
            size=15,
            positioning="physical",
        )

        self.numinput = InputNumeric((300, 275), 3.14, positioning="physical")