Repository: vispy/vispy

Issue:
Picking broken (visual_at) 
While testing current main I've found that the picking example doesn't work anymore.

examples/demo/scene/picking.py

@djhoese just confirmed on gitter chat, that it doesn't work for him too.

I'll try to bisect this tomorrow, but if anyone is faster, please do so. 


Discussion:
So I could be doing something wrong, but this example doesn't seem to work for me on the `maint/0.6` branch either.
Tag v0.6.2 works, v0.6.3 doesn't
I can't remember which version worked. But I know it worked at some point in time, because I've adapted from that example for one of my applications. Unfortunately it got lost, was not used for years now. 
https://github.com/vispy/vispy/pull/1748 broke it. Still debugging...

CC @tlambert03 
Yep, if I manually revert the `plotwidget.py` changes in that PR to what they were before then it works.
@djhoese I'm not that fast on the phone, but this is also my impression. The viewbox somehow overlays all other added plots. Strange... 
ugh :/  bummer... does https://github.com/vispy/vispy/issues/1742 come back then?
@tlambert03 Your fix is still there so I would assume not. My guess is that something with picking is assuming that the viewbox is early in the list of children which is the opposite of what you needed in #1748 since the sizing has to be bottom-up to work properly...if I remember correctly. Still debugging to see what the different ordering changes as far as picking.
Record keeping...

If you run `self._scene.describe_tree()` from the SceneCanvas, you get this in the old code:

```
SubScene
  +--Widget
     +--Grid
        +--PlotWidget
           +--Grid
              +--Widget
              +--Widget
              +--Widget
              +--Label
              +--Widget
              +--Widget
              +--Label
              +--AxisWidget
              +--ViewBox
              |  +--SubScene
              |     +--BaseCamera
              |     +--PanZoomCamera
              |     +--LinePlot
              |     +-- <... lots of LinePlot instances>
              |     +--LinePlot
              |     +--Text
              |     +--Line
              |     +--Markers
              +--AxisWidget
              +--Widget
              +--Label
              +--Widget
```

I just wanted to show what the order of things is.

Edit: In the old code (0.6.2) when doing the picking the picking sees all of the various things on the canvas. In the new method it *only* sees the ViewBox. This is strange to me as I would assume that since the LinePlots are children of the ViewBox they would be "understood" during picking.
Ok, very interesting findings now. Things to notice about this situation:

1. The `_configure_2d` method is called separately (before) the LinePlot Visual is created and added to the view. So as far as the ViewBox <-> LinePlot relationship, nothing should be different.
2. The ViewBox is being added to a GridWidget. The GridWidget doesn't do anything ordering specific as far as I can tell. It just resizes things.
3. If I move the view box creation before the `xlabel` `Label` creation in `_configure_2d` then it works. If I move it after that it fails. **BUT** if I comment out the `self.xlabel` usage it doesn't work whether I create the view right before the label code or right after. :shrug: 
Workaround: set `self.view.interactive = False` in the `_configure_2d` method.
There could be some unexpected side effects that I'm missing here, but here's some things we should know:

1. Setting `self.view.interactive = False` and having it works means that the `PickingFilter` is being used and applied properly.
2. The fact that what widgets are created and in what order suggests that the issue is the GridWidget or some of these other widgets blocking something. BUT this doesn't really make sense because the ViewBox is still the thing getting selected/picked, not one of these other widgets. It is like the ViewBox drawing (it draws its picking mesh) is somehow getting drawn before the children (the LinePlot visuals), but this, again, shouldn't be effected by the GridWidget and when widgets are added.

Edit: I forgot to add that I printed out the drawing order and as far as I can tell it is the same (or as similar as can be expected given the different order of children being added to the grid).
Ok major discovery: If you comment out the `on_mouse_move` handler in the picking example script then it finds things just fine.

Edit: Ok if you comment out the `cursor_text` stuff then it mostly works except only on every other click. This is really weird.
Current minimal reproducible example:

```python
import numpy as np
import vispy.plot as vp

# create figure with plot
fig = vp.Fig()
plt = fig[0, 0]

# plot data
x = np.linspace(0, 400, 100)
y = np.linspace(-144.1, -9.44, 100)
line = plt.plot((x, y), color='red')
line.interactive = True
fig._plot_widgets[0].view.order = -5

fig.app.process_events()
print("Selected: ", fig.visuals_at((201, 411)))
```

I'm going to try to make this even smaller and remove the PlotWidget from the situation. If you comment out the `view.order` it doesn't work and you should see only the ViewBox in the print out. If you leave it uncommented (the ViewBox is drawn before the other Fig components - axis, labels, colorbar widgets, etc) then you should see two elements in the print out; the ViewBox and the LinePlot.
This seems to make the picking work in the original example:
```diff
diff --git a/vispy/visuals/line/line.py b/vispy/visuals/line/line.py
index f709949f..1fa01df4 100644
--- a/vispy/visuals/line/line.py
+++ b/vispy/visuals/line/line.py
@@ -299,7 +299,7 @@ class _GLLineVisual(Visual):
 
         Visual.__init__(self, vcode=self.VERTEX_SHADER,
                         fcode=self.FRAGMENT_SHADER)
-        self.set_gl_state('translucent')
+        # self.set_gl_state('translucent')
 
     def _prepare_transforms(self, view):
         xform = view.transforms.get_transform()
diff --git a/vispy/visuals/markers.py b/vispy/visuals/markers.py
index bf7f07ec..54fb0e27 100644
--- a/vispy/visuals/markers.py
+++ b/vispy/visuals/markers.py
@@ -496,8 +496,8 @@ class MarkersVisual(Visual):
         Visual.__init__(self, vcode=vert, fcode=frag)
         self.shared_program.vert['v_size'] = self._v_size_var
         self.shared_program.frag['v_size'] = self._v_size_var
-        self.set_gl_state(depth_test=True, blend=True,
-                          blend_func=('src_alpha', 'one_minus_src_alpha'))
+        # self.set_gl_state(depth_test=True, blend=True,
+        #                   blend_func=('src_alpha', 'one_minus_src_alpha'))
         self._draw_mode = 'points'
         if len(kwargs) > 0:
             self.set_data(**kwargs)
```
Thanks @asnt I'll test that out. From my findings it seems to be something related to drawing order and likely the TextVisual. I also saw something related to `set_gl_state` (or `configure_gl_state` which is what TextVisual uses), but couldn't nail down what was going on.

Here is my new minimal example:

```python
import numpy as np
from vispy import scene

canvas = scene.SceneCanvas()
grid = canvas.central_widget.add_grid()

yaxis = scene.Label("")
# yaxis = scene.Widget()
yaxis_widget = grid.add_widget(yaxis, row=2, col=3)
yaxis_widget.width_max = 40

view = grid.add_view(row=2, col=4,
                     border_color='grey', bgcolor="#efefef")
view.camera = 'panzoom'
camera = view.camera

# plot data
x = np.linspace(0, 400, 100)
y = np.linspace(-144.1, -9.44, 100)
line = scene.LinePlot((x, y))
line.interactive = True
view.add(line)
view.camera.set_range()
view.order = -5
# view.interactive = False

canvas.show()
canvas.app.process_events()
print("Picked IDs: ", np.unique(canvas._render_picking((0.0, 0.0, 800, 600))))
print("Picked IDs: ", np.unique(canvas._render_picking((0.0, 0.0, 800, 600))))
from vispy.scene import VisualNode
# print(list(VisualNode._visual_ids.items()))
```

The print out is a series of IDs. In the first print out with the `view.order` set you should see three IDs. `0` means no visual there, one of the others is the ViewBox, and one of the others is the LinePlot. This is all of the IDs the rendered picking is seeing when drawing the whole Scene. Not that in the second print out the LinePlot disappears. Also notice that if you replace the Label with a plain Widget that it doesn't work.
@asnt Nice find on that gl_state stuff. If I remove (comment out) the `view.order` line in my above example and comment out the `set_gl_state` for the Markers and Line visuals then both print outs show all the expected IDs (3 total).

Edit: Simplification to the above, change the LinePlot line to:

```python
line = scene.Line(np.array((x, y)).T.astype(np.float32))
```

Edit 2: It looks like commenting out the `Line` `set_gl_state` makes the `Widget` versus `Label` case work too.
Now I'm just really confused, even this doesn't work without commenting out the `set_gl_state` for the LineVisual:

```python
import numpy as np
from vispy import scene

canvas = scene.SceneCanvas()
view = canvas.central_widget.add_view()
view.camera = 'panzoom'
camera = view.camera

x = np.linspace(0, 400, 100)
y = np.linspace(-144.1, -9.44, 100)
line = scene.Line(np.array((x, y)).T.astype(np.float32))
line.interactive = True
view.add(line)
view.camera.set_range()
# view.order = -5

canvas.show()
canvas.app.process_events()
print("Picked IDs: ", np.unique(canvas._render_picking((0.0, 0.0, 800, 600))))
print("Picked IDs: ", np.unique(canvas._render_picking((0.0, 0.0, 800, 600))))
```
This change alone (without the previous diff) also makes it work (without looking at whether it breaks the rendering):
```diff
diff --git a/vispy/scene/visuals.py b/vispy/scene/visuals.py
index 0147f17b..c4bcef30 100644
--- a/vispy/scene/visuals.py
+++ b/vispy/scene/visuals.py
@@ -68,6 +68,7 @@ class VisualNode(Node):
         self._picking = p
         self._picking_filter.enabled = p
         self.update_gl_state(blend=not p)
+        self.update_gl_state(depth_test=False)
 
     def _update_trsys(self, event):
         """Transform object(s) have changed for this Node; assign these to the
```

Note: Not related to the issue here, but I have the impression that `self.update_gl_state(blend=not p)` makes the assumption that we always have `blend=True` when not picking. Isn't that a bit strong an assumption? Or the visuals always reset the state, so it doesn't matter?
> Isn't that a bit strong an assumption? Or the visuals always reset the state, so it doesn't matter?

Yes, I think it is a strong assumption although not a bad one given how many vispy visuals use blending perhaps. It also doesn't seem like something that would be that hard to workaround (use the current blending state and change it depending on that if needed). Also, visuals typically don't re-assert their GL state. Only on `__init__`.
Pushing the ViewBox that always get picked further back also helps:
```diff
diff --git a/vispy/scene/widgets/widget.py b/vispy/scene/widgets/widget.py
index a1c44dd7..9e85b193 100644
--- a/vispy/scene/widgets/widget.py
+++ b/vispy/scene/widgets/widget.py
@@ -377,6 +377,10 @@ class Widget(Compound):
             [right, top], [right-w, top-w],
             [left, top], [left+w, top-w],
         ], dtype=np.float32)
+        if self.__class__.__name__ == "ViewBox":
+            z_behind = 1.0
+            pos_z = np.full((len(pos), 1), z_behind, dtype=np.float32)
+            pos = np.hstack((pos, pos_z))
         faces = np.array([
             [0, 2, 1],
             [1, 2, 3],
```
Ok so based on these recent "GL state" discoveries I feel like it makes sense then there is some conflict between the GL state desired by some Visuals/Widgets and those of others. That's why in my testing I was able to get it to work by reordering the draw order. I want to make sure that however we solve this that we at least *know* why it is not working even if we end up having to implement a workaround rather than a "real" solution.

Here's what I get for what widgets/visuals are made and what GL state they attempt to make by running my Line script above. These sections are in order of creation (from what I can tell).

## Regular Draw

### `SceneCanvas.central_widget` - `Widget`

The base `Widget` class makes two MeshVisuals:

https://github.com/vispy/vispy/blob/184d854c7542f4c6df21bb4c7db3dd5b340dd900/vispy/scene/widgets/widget.py#L49-L54

Mesh 1 (border): `{'depth_test': False, 'cull_face': False, 'preset': 'translucent'}`
Mesh 2 (picking): ` {'depth_test': True, 'cull_face': False, 'preset': 'translucent'}`

Note the picking mesh isn't drawn during regular draw, but the configuration is still "attempted" during creation.

### `ViewBox` - `Widget subclass`

Same as `Widget` above

### `Line`

`{'preset': 'translucent'}`

There's some line smoothing stuff later on, but not relevant here. Also see #2109.

## Final Regular Draw GL State

`{'depth_test': True, 'cull_face': False, 'blend': True, 'blend_func': ('src_alpha', 'one_minus_src_alpha', 'zero', 'one')}`

## Picking draw

Basically blend is set to False. This fails and only "sees" the ViewBox.

### Picking Mesh from ViewBox

GL state: `{'cull_face': False, 'blend': False}`

### LineVisual

GL state: `{'line_smooth': False, 'line_width': 1.0, 'blend': False, 'depth_test': True, 'cull_face': False, 'blend_func': ('src_alpha', 'one_minus_src_alpha', 'zero', 'one')}`

If I comment out the LineVisual's state configuration then we get:

GL state:  `{'line_smooth': False, 'line_width': 1.0, 'blend': False}`

and the proper picked IDs.

## Depth testing for the win

It seems like if I add `depth_test=False` to the picking mesh then this all works:

https://github.com/vispy/vispy/blob/184d854c7542f4c6df21bb4c7db3dd5b340dd900/vispy/scene/widgets/widget.py#L52-L53

It is getting late here so I may not make much sense with this explanation, but I'm not sure I understand how this ever works or worked. If I run the original `picking.py` example and move the ViewBox creation between the 2 places I don't see an obvious useful difference. That must mean (I think) that drawing order is just "getting lucky". So is `depth_test=False` on the picking mesh the right answer? :shrug: 

Edit: The last time a depth_test or other GL state change was made was in https://github.com/vispy/vispy/pull/1191 to fix some plotting issue in an example if I remember, but this was only for Markers. The depth_test was changed from False to True which would negatively effect the picking...I guess.
Nice analysis!

Nice find with #1191: Reverting it (back to `depth_test=False`) indeed makes the picking example work.

EDIT: Sorry, I just read #2109 and it seems you are well aware @djhoese . I missed this one.
EDIT2: Removed my suggestion because you had it all in your analysis. I was just repeating it. Now, I think I understand and I follow you. :+1: :+1: :+1:

Yes! I was just about to post about this. I have a better understanding of this now. And I think my last comment was 50% "I don't know when GL state is changed" and 50% "oh this is how it works" so it got extra confusing. I'll make another comment in a second with my findings.
For today's novel...ok so I have a slightly better understanding of why some cases worked and some didn't. I think this has only worked by chance most of the time. Here's what I see if I print out information about the GL state as my [LinePlot example](https://github.com/vispy/vispy/issues/2107#issuecomment-869056283).

```
From the regular draw:

About to call _configure_gl_state from Visual.draw:  <vispy.visuals.mesh.MeshVisual object at 0x7f97bc40c910>
GL state:  {'depth_test': False, 'cull_face': False, 'blend': True, 'blend_func': ('src_alpha', 'one_minus_src_alpha', 'zero', 'one')}
About to call _configure_gl_state from Visual.draw:  <vispy.visuals.line.line._GLLineVisual object at 0x7f97bc401450>
GL state:  {'depth_test': True, 'cull_face': False, 'blend': True, 'blend_func': ('src_alpha', 'one_minus_src_alpha', 'zero', 'one')}

About to call _configure_gl_state from Visual.draw:  <vispy.visuals.markers.MarkersVisual object at 0x7f97bc3a0ed0>
GL state:  {'depth_test': True, 'blend': True, 'blend_func': ('src_alpha', 'one_minus_src_alpha')}
About to call _configure_gl_state from Visual.draw:  <vispy.visuals.text.text.TextVisual object at 0x7f97bc48da50>
GL state:  {'blend': True, 'depth_test': False, 'cull_face': False, 'blend_func': ('src_alpha', 'one_minus_src_alpha')}



When it works and we use a Label widget, we get these GL states:

About to call _configure_gl_state from Visual.draw:  <vispy.visuals.mesh.MeshVisual object at 0x7f97bc4848d0>
GL state:  {'cull_face': False, 'blend': False}

About to call _configure_gl_state from Visual.draw:  <vispy.visuals.line.line._GLLineVisual object at 0x7f97bc401450>
GL state:  {'line_smooth': False, 'line_width': 1.0, 'blend': False, 'depth_test': True, 'cull_face': False, 'blend_func': ('src_alpha', 'one_minus_src_alpha', 'zero', 'one')}

About to call _configure_gl_state from Visual.draw:  <vispy.visuals.markers.MarkersVisual object at 0x7f97bc3a0ed0>
GL state:  {'depth_test': True, 'blend': False, 'blend_func': ('src_alpha', 'one_minus_src_alpha')}



The second time we call the picking:

About to call _configure_gl_state from Visual.draw:  <vispy.visuals.mesh.MeshVisual object at 0x7f97bc4848d0>
GL state:  {'cull_face': False, 'blend': False}
About to call _configure_gl_state from Visual.draw:  <vispy.visuals.line.line._GLLineVisual object at 0x7f97bc401450>
GL state:  {'line_smooth': False, 'line_width': 1.0, 'blend': False, 'depth_test': True, 'cull_face': False, 'blend_func': ('src_alpha', 'one_minus_src_alpha', 'zero', 'one')}

About to call _configure_gl_state from Visual.draw:  <vispy.visuals.markers.MarkersVisual object at 0x7f97bc3a0ed0>
GL state:  {'depth_test': True, 'blend': False, 'blend_func': ('src_alpha', 'one_minus_src_alpha')}
```

Things to note:

1. After the regular draw `depth_test` is `False` because of the `TextVisual`. This state doesn't change when the picking mesh is drawn in the next step (more on that later).
2. After the first picking, the MarkersVisual leaves `depth_test` as `True`. This state doesn't change when the picking mesh is drawn in the next step. So during the second picking the picking mesh is drawn with `depth_test` as `True`. This is why the second picking call doesn't behave as expected.
3. The picking mesh's state is only ever set as `{'cull_face': False, 'blend': False}`. This is because after it is created with `self.set_gl_state('translucent', depth_test=True, cull_face=False)` in `__init__`, the `Widget` class does `self._picking_mesh.set_gl_state(cull_face=False)`. **BUT** `gl_state` doesn't just update one parameter and keep all the old ones that were in the state. It overwrite this. The other similar issue is that if you use a preset name once, you need to use it every time if you want those preset values to be included in the GL state for this visual. This means that the depth testing and other GL state parameters for the picking mesh are completely dependent on the previously drawn visual. Since the ViewBox is usually the first thing drawn and it only draws the picking mesh during picking this means it is dependent on the last thing to be drawn from regular drawing.

### More on MarkersVisual

One thing that's interesting to me is that I brought up in my previous comment that MarkersVisual was changed from `depth_test=False` to `depth_test=True`. If this change hadn't been done then my second picking in my example above would have worked as the MarkersVisual was the last thing drawn.

Edit 2: Also note that MarkersVisual are included in every LinePlot entry as it is a Compound of a LineVisual and a MarkersVisual. It may not be drawn, but its GL state is still configured.

### Best fix

I'd say the picking mesh should either be re-configured after creation (in `Widget.__init__`) with:

```python
self._picking_mesh.set_gl_state('translucent', depth_test=False, cull_face=False)
```

Or maybe:

```python
self._picking_mesh.set_gl_state(depth_test=False, cull_face=False)
```

Thoughts?

Edit: Put another way, I think it should be considered best practice to specify all parameters (or a preset) when you change/set/update GL state. This will technically effect performance as it means more GLIR commands which means more GL calls, but otherwise I think you are dependent on the order that you draw things in and what those other things set for GL state.
> 2. After the first picking, the MarkersVisual leaves `depth_test` as `True`. This state doesn't change when the picking mesh is drawn in the next step. So during the second picking the picking mesh is drawn with `depth_test` as `True`. This is why the second picking call doesn't behave as expected.

I have the impression that the root cause of this is that the `Widget.picking_mesh` of the `ViewBox` (derived of `Widget`) is covering the entire viewport. So, when `depth_test=True` in picking mode, the viewbox, drawn first, prevents the child visuals to be drawn, because they are all at the same depth and the entire depth buffer has already been written to by the view box.

In this 2D plotting context, if the end goal is that the foreground visuals (lines, markers...) be over the background (viewbox), it seems to me that there can be several strategies:
1. (The intended current one?) Set `depth_test=False` and draw the visuals from furthest to closest (~~using `.order`~~ EDIT: See below.). (The actual depth position of the visuals, if any, has no effect.)
2. Set `depth_test=True` and draw the visuals from closest to furthest (~~using `.order`~~ EDIT: See below.). (Assuming a common depth position for all visuals.)
3. Arrange the visuals in foreground/background layers with different `z` values, e.g. `pos=(x, y, z0)`, `pos=(x, y, z1)`.... Render with `depth_test=True`. The drawing order has no effect.

EDIT: Drawing order in strategy 1.
EDIT2: Revert incorrect first edit and add strategy 2.
EDIT3: Actually `.order` is not enough because it represents the order between the children of a single node (at a single level of the hierarchy), not an absolute order in the scene. So, only strategy 1 would work if the child visuals are always more into the foreground. Strategy 2 seems difficult to implement (if at all possible) with the current scene graph and picking models.
> Edit 2: Also note that MarkersVisual are included in every LinePlot entry as it is a Compound of a LineVisual and a MarkersVisual. It may not be drawn, but its GL state is still configured.

Shouldn't the `self._configure_gl_state()` call come after the next `if` block, to not unnecessarily send glir commands:
https://github.com/vispy/vispy/blob/184d854c7542f4c6df21bb4c7db3dd5b340dd900/vispy/visuals/visual.py#L439-L444

EDIT: Already being addressed in #2109. Sorry.
> I'd say the picking mesh should either be re-configured after creation (in `Widget.__init__`) with:
> 
> ```python
> self._picking_mesh.set_gl_state('translucent', depth_test=False, cull_face=False)
> ```
> 
> Or maybe:
> 
> ```python
> self._picking_mesh.set_gl_state(depth_test=False, cull_face=False)
> ```

It seems like the adequate fix to me. The second one seems sufficient because `blending=False` is taken care of by the picking logic, and `blend_func` doesn't matter then).
Also, to me this assumes strategy one above. Right?


> The second one seems sufficient because blending=False is taken care of by the picking logic, and blend_func doesn't matter then).

Oh good point. At that point there aren't any unspecified settings.

I think you've addressed any concerns I had with the z-position stuff. I think it just ends up being difficult to safely implement without telling the user they need to setup their SceneCanvas a certain way if they want picking to work.

Some other things I want to point out:

1. I'm 99% sure the ViewBox doesn't take up the whole viewport. In my examples with the plotting API or where a GridWidget is used, the Visuals/Widgets that are not the ViewBox are not drawn during picking and show up as 0s. In one of my examples above the rendered picking array that I get the unique values for includes 0. When I view it in pycharm's debugger I see that it is a border of 0s.
2. The only reason these ViewBox's child Visuals are drawn during picking is because their `.interactive = True` property is set. Without this they are skipped for picking.
3. We should be able to assume (as it is how it is coded) that the ViewBox picking Mesh is always drawn before the children under the ViewBox.

The other option I thought about would be along the lines of your Z-level stuff where we make the ViewBox picking mesh go all the way back; something like a max-z level so it is always behind everything else. However, I think this still depends on all of the other transforms and how the Visuals are set up.

I guess I'll make a PR for `self._picking_mesh.set_gl_state(depth_test=False, cull_face=False)` and we'll see how the tests go and what other thoughts people have.

Resolving patch:
diff --git a/vispy/scene/widgets/widget.py b/vispy/scene/widgets/widget.py
--- a/vispy/scene/widgets/widget.py
+++ b/vispy/scene/widgets/widget.py
@@ -50,7 +50,7 @@ def __init__(self, pos=(0, 0), size=(10, 10), border_color=None,
         self._mesh.set_gl_state('translucent', depth_test=False,
                                 cull_face=False)
         self._picking_mesh = MeshVisual(mode='triangle_fan')
-        self._picking_mesh.set_gl_state(cull_face=False)
+        self._picking_mesh.set_gl_state(cull_face=False, depth_test=False)
         self._picking_mesh.visible = False
 
         # reserved space inside border
diff --git a/vispy/visuals/mesh.py b/vispy/visuals/mesh.py
--- a/vispy/visuals/mesh.py
+++ b/vispy/visuals/mesh.py
@@ -358,9 +358,6 @@ def _prepare_draw(self, view):
                 return False
             self._data_changed = False
 
-    def draw(self, *args, **kwds):
-        Visual.draw(self, *args, **kwds)
-
     @staticmethod
     def _prepare_transforms(view):
         tr = view.transforms.get_transform()


Test patch:
diff --git a/vispy/scene/tests/test_canvas.py b/vispy/scene/tests/test_canvas.py
--- a/vispy/scene/tests/test_canvas.py
+++ b/vispy/scene/tests/test_canvas.py
@@ -60,3 +60,31 @@ def test_canvas_render(blend_func):
         else:
             # the alpha should have some transparency
             assert (rgba_result[..., 3] != 255).any()
+
+
+@requires_application()
+def test_picking_basic():
+    """Test basic picking behavior.
+
+    Based on https://github.com/vispy/vispy/issues/2107.
+
+    """
+    with TestingCanvas(size=(125, 125), show=True, title='run') as c:
+        view = c.central_widget.add_view()
+        view.margin = 5  # add empty space where there are no visuals
+        view.camera = 'panzoom'
+
+        x = np.linspace(0, 400, 100)
+        y = np.linspace(0, 200, 100)
+        line = scene.Line(np.array((x, y)).T.astype(np.float32))
+        line.interactive = True
+        view.add(line)
+        view.camera.set_range()
+
+        c.render()  # initial basic draw
+        for _ in range(2):  # run picking twice to make sure it is repeatable
+            # get Visuals on a Canvas point that Line is drawn on
+            picked_visuals = c.visuals_at((100, 25))
+            assert len(picked_visuals) == 2
+            assert any(isinstance(vis, scene.ViewBox) for vis in picked_visuals)
+            assert any(isinstance(vis, scene.Line) for vis in picked_visuals)