No vertical scroll if started on horizontal ScrollView embeded in vertical ScrollView

26 views
Skip to first unread message

Andreas Ecker

unread,
May 24, 2025, 10:47:56 AMMay 24
to kivy-...@googlegroups.com
When I embed a horizontal ScrollView into a vertical ScrollView, then the vertical scroll does not work if the scroll/drag gets started with the mouse pointer within the screen area of the horizontal ScrollView.

Also any vertical scrolls via the mouse wheel does not work if the mouse pointer is within/above the horizontal ScrollView.

The following example app code is demonstrating the problem (try to vertically scroll with the mouse pointer in the gray/horizontal screen area):

```
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder

KV = """
<RootWidget>:
# Outer ScrollView (Vertical)
ScrollView:
id: outer_scroll
do_scroll_x: False # Only scroll vertically
do_scroll_y: True

GridLayout:
id: outer_content
cols: 1
size_hint_y: None
height: self.minimum_height # Make content take its needed height
padding: dp(10)
spacing: dp(10)

Label:
text: "Top Content"
size_hint_y: None
height: dp(100)
canvas.before:
Color:
rgba: 0.2, 0.2, 0.8, 0.5
Rectangle:
pos: self.pos
size: self.size

# Inner ScrollView (Horizontal)
ScrollView:
id: inner_scroll
size_hint_y: None # Inner ScrollView needs a defined height
height: dp(120) # Example height for the horizontal scroll area
do_scroll_y: False # CRITICAL: Only scroll horizontally
do_scroll_x: True
#bar_width: dp(10) # Optional: make scrollbar visible

GridLayout:
id: inner_content
rows: 1
size_hint_x: None
width: self.minimum_width # Make content take its needed width
spacing: dp(5)

Button:
text: "Item 1"
size_hint_x: None
width: dp(150)
Button:
text: "Item 2"
size_hint_x: None
width: dp(150)
Button:
text: "Item 3"
size_hint_x: None
width: dp(150)
Button:
text: "Item 4"
size_hint_x: None
width: dp(150)
Button:
text: "Item 5"
size_hint_x: None
width: dp(150)
Button:
text: "Item 6"
size_hint_x: None
width: dp(150)

Label:
text: "Middle Content 1"
size_hint_y: None
height: dp(200)
canvas.before:
Color:
rgba: 0.2, 0.8, 0.2, 0.5
Rectangle:
pos: self.pos
size: self.size

Label:
text: "Middle Content 2"
size_hint_y: None
height: dp(200)
canvas.before:
Color:
rgba: 0.8, 0.2, 0.2, 0.5
Rectangle:
pos: self.pos
size: self.size

Label:
text: "Bottom Content"
size_hint_y: None
height: dp(300) # Make this large enough to ensure vertical scroll
canvas.before:
Color:
rgba: 0.8, 0.8, 0.2, 0.5
Rectangle:
pos: self.pos
size: self.size
"""


class RootWidget(BoxLayout): # This is a BoxLayout, its content is defined in KV
pass


class NestedScrollApp(App):
def build(self):
Builder.load_string(KV) # Load the KV definitions
return RootWidget() # Instantiate and return the root widget
# Kivy will apply the <RootWidget> rule from KV to this instance.


if __name__ == '__main__':
NestedScrollApp().run()
```


ElliotG

unread,
May 24, 2025, 11:07:22 AMMay 24
to Kivy users support

Andreas Ecker

unread,
May 26, 2025, 8:45:40 AMMay 26
to kivy-...@googlegroups.com
Thank you very much @ElliotG for the issue links !!!

This workaround of @mz3r0 in issue #8926 looked promising - so I did some similar tests by subclassing the outer ScrollView, to forward the touch down events first to the inner ScrollView:

```
def sv_children(parent: Widget) -> list[Widget]:
svs = []
for chi in parent.children:
svs.extend(sv_children(chi))
if isinstance(chi, ScrollView):
svs.append(chi)
return svs

class EmbeddingScrollView(ScrollView):
def on_touch_down(self, touch: MotionEvent):
inner_scroll_views = sv_children(self)
print(f"on_touch_down on {self.do_scroll_x=} {self.do_scroll_y=} {inner_scroll_views=} of {self=}")
for inner_sv in inner_scroll_views:
coords = inner_sv.to_parent(*inner_sv.to_widget(*touch.pos))
print(f"{inner_sv=} {inner_sv.collide_point(*coords)=} {inner_sv.scroll_x=} {inner_sv.scroll_y=}")
if inner_sv.collide_point(*coords): # and inner_sv.scroll_x not in (1, 0):
touch.push()
touch.apply_transform_2d(inner_sv.to_widget)
touch.apply_transform_2d(inner_sv.to_parent)
consumed = inner_sv.on_touch_down(touch)
touch.pop()
if consumed:
print(" === touch event consumed")
return True
return super().on_touch_down(touch)
```
But unfortunately it didn't fully fix the problem. But I will keep up fighting this bug.

Maybe better to wait until the PR #9063 of @akshayaurora (IIRC one of the founders of Kivy) gets merged in (which is also changing the touch event handlers of the ScrollView widget).

Meanwhile, and to better understand how the event handling and bubbling is working in Kivy I made a new bug demo app, which outputs lots of useful tracing/debug info to the console:

```
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget


KV = """
RootWidget:
# Outer ScrollView (Vertical)
    TracedSV:
id: outer_scroll
tt: "Out"
        do_scroll_x: False  # Only scroll vertically
do_scroll_y: True

GridLayout:
id: outer_content
cols: 1
size_hint_y: None
height: self.minimum_height # Make content take its needed height
padding: dp(10)
spacing: dp(10)

            TracedLab:
text: "Top Label"
tt: "Top"
                size_hint_y: None
height: dp(100)
canvas.before:
Color:
rgba: 0.2, 0.2, 0.8, 0.5
Rectangle:
pos: self.pos
size: self.size

# Inner ScrollView (Horizontal)
            TracedSV:
id: inner_scroll
tt: "Inn"
                size_hint_y: None       # Inner ScrollView needs a defined height
height: dp(120) # Example height for the horizontal scroll area
do_scroll_y: False # CRITICAL: Only scroll horizontally
do_scroll_x: True
#bar_width: dp(10) # Optional: make scrollbar visible

GridLayout:
id: inner_content
rows: 1
size_hint_x: None
width: self.minimum_width # Make content take its needed width
spacing: dp(5)

                    TracedLab:
text: "Left Label"
tt: "Lef"
size_hint_x: None
width: dp(150)
TracedWid:
tt: "Lef"
size_hint_x: None
width: dp(150)
TracedBut:
text: "Left Button"
tt: "Lef"
size_hint_x: None
width: dp(150)
TracedLab:
text: "Right Label"
tt: "Rig"
size_hint_x: None
width: dp(150)
TracedWid:
tt: "Rig"
size_hint_x: None
width: dp(150)
TracedBut:
text: "Right Button"
tt: "Rig"
size_hint_x: None
width: dp(150)

TracedLab:
text: "Middle Label"
tt: "Mid"
                size_hint_y: None
height: dp(200)
canvas.before:
Color:
rgba: 0.2, 0.8, 0.2, 0.5
Rectangle:
pos: self.pos
size: self.size

            TracedLab:
text: "Bottom Label"
tt: "Btm"
size_hint_y: None
height: dp(600) # Make this large enough to ensure vertical scroll
                canvas.before:
Color:
rgba: 0.8, 0.8, 0.2, 0.5
Rectangle:
pos: self.pos
size: self.size
"""


class RootWidget(BoxLayout): # This is a BoxLayout, its content is defined in KV
pass


class TracedSV(ScrollView):
def on_touch_down(self, touch):
ret = super().on_touch_down(touch)
print(f"Scv {self.tt} Dn={'y' if ret else 'n'} {touch=}")
return ret

def on_touch_move(self, touch):
ret = super().on_touch_move(touch)
print(f"Scv {self.tt} Mv={'y' if ret else 'n'} {touch=}")
return ret

def on_touch_up(self, touch):
ret = super().on_touch_up(touch)
print(f"Scv {self.tt} Up={'y' if ret else 'n'} {touch=}")
return ret


class TracedBut(Button):
def on_touch_down(self, touch):
ret = super().on_touch_down(touch)
print(f"But {self.tt} Dn={'y' if ret else 'n'} {touch=}")
return ret

def on_touch_move(self, touch):
ret = super().on_touch_move(touch)
print(f"But {self.tt} Mv={'y' if ret else 'n'} {touch=}")
return ret

def on_touch_up(self, touch):
ret = super().on_touch_up(touch)
print(f"But {self.tt} Up={'y' if ret else 'n'} {touch=}")
return ret


class TracedLab(Label):
def on_touch_down(self, touch):
ret = super().on_touch_down(touch)
print(f"Lab {self.tt} Dn={'y' if ret else 'n'} {touch=}")
return ret

def on_touch_move(self, touch):
ret = super().on_touch_move(touch)
print(f"Lab {self.tt} Mv={'y' if ret else 'n'} {touch=}")
return ret

def on_touch_up(self, touch):
ret = super().on_touch_up(touch)
print(f"Lab {self.tt} Up={'y' if ret else 'n'} {touch=}")
return ret


class TracedWid(Widget):
def on_touch_down(self, touch):
ret = super().on_touch_down(touch)
print(f"Wid {self.tt} Dn={'y' if ret else 'n'} {touch=}")
return ret

def on_touch_move(self, touch):
ret = super().on_touch_move(touch)
print(f"Wid {self.tt} Mv={'y' if ret else 'n'} {touch=}")
return ret

def on_touch_up(self, touch):
ret = super().on_touch_up(touch)
print(f"Wid {self.tt} Up={'y' if ret else 'n'} {touch=}")
return ret


class NestedScrollApp(App):
def build(self):
return Builder.load_string(KV)



if __name__ == '__main__':
NestedScrollApp().run()
```

--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/kivy-users/371e11e3-8cf3-4dce-848f-93812f038fdcn%40googlegroups.com.

ElliotG

unread,
May 26, 2025, 9:23:35 AMMay 26
to Kivy users support
I had started to take a look at this issue some months ago using cursor (AI assisted coding editor).  I found cursor to be very helpful for describing the ScrollView source code and the use of the dictionary used to track the touches.  I have not had a chance to get back to it.  You might find it useful if you decide to work on this.
Reply all
Reply to author
Forward
0 new messages