Repository: kivy/kivy

Issue:
CollideWidget does not work for widgets with negative size
**Software Versions**
* Python: 3.7.9
* OS:  Ubuntu 16.04
* Kivy: 1.11.1
* Kivy installation method: `pip install kivy`

**Describe the bug**
If the width and/or height of a widget is negative, `collide_widget` cannot accurately detect collisions. The method assumes `self.right > self.x` and `self.top > self.y` which is not true when `self.width < 0` or `self.height < 0`
```
# kivy.uix.widget
class Widget(WidgetBase):
    ...
    def collide_widget(self, wid):
       """..."""
        if self.right < wid.x:
            return False
        if self.x > wid.right:
            return False
        if self.top < wid.y:
            return False
        if self.y > wid.top:
            return False
        return True
```

**Expected behavior**
`collide_widget` detects collisions no matter what the dimensions of the widget are

**To Reproduce**
see code below

**Code and Logs and screenshots**
The below widgets are clearly  colliding but `collide_widget` cannot detect it because of the above stated bug. Below is the code that produced this screenshot.
![kivy_issue_7226](https://user-images.githubusercontent.com/7256157/99896790-c7420100-2c8b-11eb-83ec-e02282d4381d.png)


```python
from kivy.app import App
from kivy.lang import Builder

kv = """
<TextBoundLabel@Label>:
    size_hint: None, None
    size: self.texture_size

<LabeledWidget@Widget>:
    size_hint: None, None
    canvas:
        Color:
            rgb: 1, 0, 0, 0.1
        Rectangle:
            pos: self.pos
            size: self.size
    
    TextBoundLabel:
        text: "Bottom Left"
        pos: self.parent.pos
    
    TextBoundLabel:
        text: "Bottom Right"
        pos: self.parent.right, self.parent.y
    
    TextBoundLabel:
        text: "Top Left"
        pos: self.parent.x, self.parent.top
    
    TextBoundLabel:
        text: "Top Right"
        pos: self.parent.right, self.parent.top

FloatLayout:
    Label:
        pos_hint: {"center_x": 0.5, "center_y": 0.9}
        text: f"Is there a collision?: {positive_wid.collide_widget(negative_wid)}"
    
    LabeledWidget:
        id: positive_wid
        pos_hint: {"center_x": 0.5, "center_y": 0.5}
        
        Label:
            pos: positive_wid.pos
            text: "Positive Widget"
    
    LabeledWidget:
        id: negative_wid
        size: -100, -100
        pos_hint: {"center_x": 0.6, "center_y": 0.6}
        
        Label:
            pos: negative_wid.right, negative_wid.top  # notice how I didn't use self.pos because the position would be wrong
            text: "Negative Widget"
"""


class MyApp(App):
    def build(self):
        return Builder.load_string(kv)


if __name__ == '__main__':
    MyApp().run()

```
I was able to fix this by overwriting `collide_widget` like so:
```python
    def collide_widget(self, wid: Widget) -> bool:
        """patch for negative dimensions"""
        if max(self.right, self.x) < min(wid.x, wid.right):
            return False
        if min(self.x, self.right) > max(wid.right, wid.x):
            return False
        if max(self.top, self.y) < min(wid.y, wid.top):
            return False
        if min(self.y, self.top) > max(wid.top, wid.y):
            return False
        return True
```
**Additional context**
I would've just submitted a PR patching `collide_widget`, but I suspect the code base assumes `self.right` is always right of `self.x` and `self.top` is always above `self.y`. What are the core devs thoughts on this? I see two options:
1. Modify the definition of `right` and `top` such that they are always the right edge and top edge of a widget. This will potentially fix unknown bugs in the code base that make the above position attribute assumptions.
1. Patch `collide_widget` as describe above and let the programmer deal with the consequences of a negative dimensions.


Discussion:
Do you have a use case for negative sizes?

I expect that we just don't support negative sizes.
@matham Drag selection as shown below:
![Drag selecting items in a folder](https://www.computerhope.com/issues/pictures/drag-and-select.gif)
This is standard behavior for a significant amount of desktop applications. The selection rectangle starts at the top left corner, so it would have a negative height in Kivy. I explain how forcing its position to be bottom left is non-elegant below.

To me, the most sensible way to do this in Kivy is to make a `SelectionWidget` that changes size as the mouse drags over the items the end user wants to select. Widgets that collide with the `SelectionWidget` would then be appended to some list that can be used in other parts of the app. The most straight forward way to resize the `SelectionWidget` would be to initialize the position `on_touch_down` and then change the `height` and `width` during `on_touch_move` events. Here's a code snippet:
```python
def on_touch_down(self, touch):
    ...
    touch.grab(self)
    self.pos = touch.pos
    ...
    return super(SelectionWidget, self).on_touch_down(touch)


def on_touch_move(self, touch):
    if touch.grab_current is self:
        ...
        self.width += touch.dx
        self.height += touch.dy
        ...
    return super(SelectionWidget, self).on_touch_move(touch)
```
Here's a gif showing this implementation. If I use `collide_widget` to detect selected widgets, I don't get the expected result. A widget should turn blue when collided.
![collide_widget_fail](https://user-images.githubusercontent.com/7256157/99916660-50e0e580-2d03-11eb-94d5-905d7dc29c3e.gif)
In this specific case, the bug is caused by `self.pos = touch.pos` in `on_touch_down`. Kivy assumes `self.pos` is the bottom left corner, but the code above only updates `width` and `height`. The programmer could update `self.pos` so `self.right` and `self.top` have the expected relationship with `self.pos`, but there is an inflection point when the size dimensions shift from being positive -> zero -> negative and vice versa. Ensuring `self.pos` is always the bottom left corner would not be very elegant and in my opinion, if Kivy allows sizes to be negative, it should provide support for negative sizes at least for collisions.

If the devs don't want to support negative sizes, there should at least be a warning in the documentation that negative dimensions aren't supported, or better yet, an error or warning raised when a widget has negative dimensions.
I think that the expectation in Kivy is that we don't support negative size, and if you're in a situation where you start selecting outside a widget, instead of doing negative size you need to properly change the selection rectangle so it's always positive. It shouldn't be hard in the controller to do that and change the start/end of the rectangle so it's always positive.

Kivy's philosophy is similar to the rest of python, where we don't warn or raise and error for things that are not officially support, but could work if you know what you're doing. Negative sizes is such a scenario - although it's not officially support it, we don't disable it explicitly.

However, adding a a note to the docs is welcome.

Resolving patch:
diff --git a/kivy/uix/widget.py b/kivy/uix/widget.py
--- a/kivy/uix/widget.py
+++ b/kivy/uix/widget.py
@@ -1053,6 +1053,9 @@ def get_window_matrix(self, x=0, y=0):
         Keep in mind that the `width` property is subject to layout logic and
         that this has not yet happened at the time of the widget's `__init__`
         method.
+
+    .. warning::
+        A negative width is not supported.
     '''
 
     height = NumericProperty(100)
@@ -1065,6 +1068,9 @@ def get_window_matrix(self, x=0, y=0):
         Keep in mind that the `height` property is subject to layout logic and
         that this has not yet happened at the time of the widget's `__init__`
         method.
+
+    .. warning::
+        A negative height is not supported.
     '''
 
     pos = ReferenceListProperty(x, y)


Test patch: