mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-27 18:03:48 +08:00
mici ux
This commit is contained in:
@@ -79,6 +79,10 @@ class BluetoothDeviceButton(BigButton):
|
||||
super().set_touch_valid_callback(lambda: touch_callback() and not self._forget_btn.is_pressed)
|
||||
self._forget_btn.set_touch_valid_callback(touch_callback)
|
||||
|
||||
def set_touch_event_valid_callback(self, touch_callback):
|
||||
super().set_touch_event_valid_callback(touch_callback)
|
||||
self._forget_btn.set_touch_event_valid_callback(touch_callback)
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
self._label.set_color(LABEL_COLOR)
|
||||
label_rect = rl.Rectangle(self._rect.x + self.LABEL_PADDING, btn_y + self.LABEL_VERTICAL_PADDING,
|
||||
|
||||
@@ -7,7 +7,7 @@ from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.action_state import should_show_forget_button
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfirmationDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, normalize_ssid
|
||||
@@ -194,6 +194,10 @@ class WifiButton(BigButton):
|
||||
super().set_touch_valid_callback(lambda: touch_callback() and not self._forget_btn.is_pressed)
|
||||
self._forget_btn.set_touch_valid_callback(touch_callback)
|
||||
|
||||
def set_touch_event_valid_callback(self, touch_callback: Callable[[MouseEvent], bool]) -> None:
|
||||
super().set_touch_event_valid_callback(touch_callback)
|
||||
self._forget_btn.set_touch_event_valid_callback(touch_callback)
|
||||
|
||||
@property
|
||||
def _is_saved(self):
|
||||
return self._wifi_manager.is_connection_saved(self._network.ssid)
|
||||
|
||||
@@ -349,7 +349,7 @@ class BigMultiOptionDialog(NavWidget):
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent) -> None:
|
||||
super()._handle_mouse_event(mouse_event)
|
||||
if not self._scroll_inner.scroll_panel.is_touch_valid():
|
||||
if not self._scroll_inner.scroll_panel.is_event_touch_valid(mouse_event):
|
||||
self._can_click = False
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
|
||||
@@ -72,9 +72,7 @@ class GuiScrollPanel2:
|
||||
|
||||
bounds_size = bounds.width if self._horizontal else bounds.height
|
||||
|
||||
for mouse_event in gui_app.mouse_events:
|
||||
self._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
|
||||
self._previous_mouse_event = mouse_event
|
||||
self._process_mouse_events(bounds, bounds_size, content_size)
|
||||
|
||||
wheel_move = rl.get_mouse_wheel_move()
|
||||
if wheel_move != 0 and self.enabled and rl.check_collision_point_rec(rl.get_mouse_position(), bounds):
|
||||
@@ -93,6 +91,11 @@ class GuiScrollPanel2:
|
||||
print()
|
||||
return self.get_offset()
|
||||
|
||||
def _process_mouse_events(self, bounds: rl.Rectangle, bounds_size: float, content_size: float) -> None:
|
||||
for mouse_event in gui_app.mouse_events:
|
||||
self._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
|
||||
self._previous_mouse_event = mouse_event
|
||||
|
||||
def _get_offset_bounds(self, bounds_size: float, content_size: float) -> tuple[float, float]:
|
||||
"""Returns (max_offset, min_offset) for the given bounds and content size."""
|
||||
return 0.0, min(0.0, bounds_size - content_size)
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
from openpilot.system.ui import widgets
|
||||
from openpilot.system.ui.lib import scroll_panel2
|
||||
from openpilot.system.ui.lib.application import MouseEvent, MousePos, gui_app
|
||||
from openpilot.system.ui.widgets.scroller import _Scroller
|
||||
from openpilot.system.ui.widgets.scroller import _Scroller, _MiciScrollPanel
|
||||
|
||||
|
||||
class Item(widgets.Widget):
|
||||
@@ -35,8 +35,9 @@ def make_scroller(monkeypatch):
|
||||
monkeypatch.setattr(widgets.device, "awake", True)
|
||||
monkeypatch.setattr(scroll_panel2, "TICI", True)
|
||||
|
||||
def make(**kwargs):
|
||||
scroller = _Scroller([Item() for _ in range(8)], scroll_indicator=False, edge_shadows=False, **kwargs)
|
||||
def make(items=None, **kwargs):
|
||||
scroller = _Scroller(items if items is not None else [Item() for _ in range(8)],
|
||||
scroll_indicator=False, edge_shadows=False, **kwargs)
|
||||
scroller.set_rect(rl.Rectangle(0, 0, 536, 240))
|
||||
scroller.scroll_panel.set_offset(-500)
|
||||
scroller.render()
|
||||
@@ -95,12 +96,268 @@ def test_drag_outside_viewport_does_not_resume_snapping(make_scroller):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("speed, expected_clicks", [(25, 1), (119, 1), (121, 0)])
|
||||
def test_inertial_scroll_click_threshold_is_unchanged(make_scroller, speed, expected_clicks):
|
||||
@pytest.mark.parametrize("single_batch", [True, False])
|
||||
def test_inertial_scroll_click_threshold_is_unchanged(make_scroller, speed, expected_clicks, single_batch):
|
||||
scroller = make_scroller()
|
||||
scroller.scroll_panel._state = scroll_panel2.ScrollState.AUTO_SCROLL
|
||||
scroller.scroll_panel._velocity = -speed
|
||||
|
||||
frame(scroller, pressed=True)
|
||||
frame(scroller, released=True, t=1.05)
|
||||
if single_batch:
|
||||
batch(scroller, event(pressed=True), event(released=True, t=1.05))
|
||||
else:
|
||||
frame(scroller, pressed=True)
|
||||
frame(scroller, released=True, t=1.05)
|
||||
|
||||
assert sum(item.clicks for item in scroller.items) == expected_clicks
|
||||
|
||||
|
||||
def event(x=200, y=100, *, pressed=False, released=False, slot=0, t=1.0):
|
||||
return MouseEvent(MousePos(x, y), slot, pressed, released, not released, t)
|
||||
|
||||
|
||||
def batch(scroller, *events):
|
||||
gui_app._mouse_events = list(events)
|
||||
scroller.render()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("horizontal", [True, False])
|
||||
@pytest.mark.parametrize("single_batch", [True, False])
|
||||
def test_drag_release_never_clicks(make_scroller, horizontal, single_batch):
|
||||
scroller = make_scroller(horizontal=horizontal)
|
||||
move = (lambda value, **kw: event(x=value, **kw)) if horizontal else (lambda value, **kw: event(y=value, **kw))
|
||||
start = 200 if horizontal else 100
|
||||
events = [move(start, pressed=True), move(start - 20, t=1.01), move(start - 40, t=1.21),
|
||||
move(start - 40, released=True, t=1.22)]
|
||||
if single_batch:
|
||||
batch(scroller, *events)
|
||||
else:
|
||||
batch(scroller, events[0])
|
||||
batch(scroller, *events[1:])
|
||||
|
||||
assert sum(item.clicks for item in scroller.items) == 0
|
||||
assert not any(item.is_pressed for item in scroller.items)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("same_batch", [True, False])
|
||||
@pytest.mark.parametrize("pc", [True, False])
|
||||
def test_drag_then_tap_activates_only_the_tap(make_scroller, monkeypatch, same_batch, pc):
|
||||
scroller = make_scroller()
|
||||
monkeypatch.setattr(widgets, "PC", pc)
|
||||
batch(scroller, event(pressed=True))
|
||||
drag = [event(180, t=1.01), event(160, t=1.21), event(160, released=True, t=1.22)]
|
||||
tap = [event(160, pressed=True, t=1.30), event(160, released=True, t=1.35)]
|
||||
if same_batch:
|
||||
batch(scroller, *drag, *tap)
|
||||
else:
|
||||
batch(scroller, *drag)
|
||||
batch(scroller, *tap)
|
||||
|
||||
assert sum(item.clicks for item in scroller.items) == 1
|
||||
|
||||
|
||||
def test_tap_before_drag_in_same_batch_is_preserved(make_scroller):
|
||||
scroller = make_scroller()
|
||||
batch(scroller, event(pressed=True), event(released=True, t=1.05),
|
||||
event(pressed=True, t=1.10), event(180, t=1.12))
|
||||
|
||||
assert sum(item.clicks for item in scroller.items) == 1
|
||||
assert scroller.scroll_panel.state == scroll_panel2.ScrollState.MANUAL_SCROLL
|
||||
|
||||
|
||||
def test_batched_tap_cancels_programmatic_scroll(make_scroller):
|
||||
scroller = make_scroller()
|
||||
scroller.scroll_to(3, smooth=True)
|
||||
batch(scroller, event(pressed=True), event(released=True, t=1.05))
|
||||
|
||||
assert not scroller.is_auto_scrolling
|
||||
assert sum(item.clicks for item in scroller.items) == 1
|
||||
|
||||
|
||||
def test_secondary_touch_does_not_change_drag_or_velocity(make_scroller):
|
||||
scroller = make_scroller()
|
||||
batch(scroller, event(pressed=True), event(180, t=1.02))
|
||||
offset = scroller.scroll_panel.get_offset()
|
||||
batch(scroller, event(400, slot=1, pressed=True, t=1.025), event(160, t=1.04),
|
||||
event(420, slot=1, t=1.045), event(420, slot=1, released=True, t=1.05))
|
||||
|
||||
assert scroller.scroll_panel.state == scroll_panel2.ScrollState.MANUAL_SCROLL
|
||||
assert scroller.scroll_panel.get_offset() == pytest.approx(offset - 20)
|
||||
assert scroller.scroll_panel._velocity == pytest.approx(-1000)
|
||||
|
||||
|
||||
def test_secondary_touch_does_not_cancel_primary_tap(make_scroller):
|
||||
scroller = make_scroller()
|
||||
batch(scroller, event(pressed=True), event(400, slot=1, pressed=True, t=1.01),
|
||||
event(400, slot=1, released=True, t=1.02), event(released=True, t=1.05))
|
||||
|
||||
assert sum(item.clicks for item in scroller.items) == 1
|
||||
|
||||
|
||||
def test_outside_press_does_not_capture_coasting_panel(make_scroller):
|
||||
scroller = make_scroller()
|
||||
panel = scroller.scroll_panel
|
||||
panel._state = scroll_panel2.ScrollState.AUTO_SCROLL
|
||||
panel._velocity = -500
|
||||
offset = panel.get_offset()
|
||||
batch(scroller, event(600, pressed=True), event(200, t=1.01), event(200, released=True, t=1.02))
|
||||
|
||||
assert panel.state == scroll_panel2.ScrollState.AUTO_SCROLL
|
||||
assert panel.get_offset() == pytest.approx(offset - 500 / 60)
|
||||
assert sum(item.clicks for item in scroller.items) == 0
|
||||
|
||||
|
||||
def test_outside_press_does_not_clear_snap_target(make_scroller):
|
||||
scroller = make_scroller()
|
||||
panel = scroller.scroll_panel
|
||||
panel._state = scroll_panel2.ScrollState.AUTO_SCROLL
|
||||
panel.snap_interval = 100
|
||||
panel._snap_target = -600
|
||||
batch(scroller, event(600, pressed=True))
|
||||
|
||||
assert panel._snap_target == -600
|
||||
assert panel.state == scroll_panel2.ScrollState.AUTO_SCROLL
|
||||
|
||||
|
||||
def test_release_crossing_drag_threshold_does_not_leave_drag_active(make_scroller):
|
||||
scroller = make_scroller()
|
||||
batch(scroller, event(pressed=True), event(160, released=True, t=1.05))
|
||||
|
||||
assert sum(item.clicks for item in scroller.items) == 0
|
||||
assert scroller.scroll_panel.state == scroll_panel2.ScrollState.STEADY
|
||||
batch(scroller, event(pressed=True, t=1.10), event(released=True, t=1.15))
|
||||
assert sum(item.clicks for item in scroller.items) == 1
|
||||
|
||||
|
||||
def test_disabling_scrolling_preserves_control_taps(make_scroller):
|
||||
scroller = make_scroller()
|
||||
scroller.set_scrolling_enabled(False)
|
||||
offset = scroller.scroll_panel.get_offset()
|
||||
batch(scroller, event(pressed=True), event(released=True, t=1.05))
|
||||
|
||||
assert sum(item.clicks for item in scroller.items) == 1
|
||||
assert scroller.scroll_panel.get_offset() == offset
|
||||
|
||||
|
||||
@pytest.mark.parametrize("restart", ["show", "enable"])
|
||||
def test_interrupted_drag_does_not_poison_next_tap(make_scroller, restart):
|
||||
scroller = make_scroller()
|
||||
batch(scroller, event(pressed=True), event(180, t=1.02))
|
||||
if restart == "show":
|
||||
scroller.set_reset_scroll_at_show(False)
|
||||
scroller.hide_event()
|
||||
scroller.show_event()
|
||||
else:
|
||||
scroller.set_enabled(False)
|
||||
batch(scroller)
|
||||
scroller.set_enabled(True)
|
||||
batch(scroller, event(pressed=True, t=1.10), event(released=True, t=1.15))
|
||||
|
||||
assert sum(item.clicks for item in scroller.items) == 1
|
||||
|
||||
|
||||
def test_mici_panel_is_not_used_by_shared_panel_callers(make_scroller):
|
||||
assert isinstance(make_scroller().scroll_panel, _MiciScrollPanel)
|
||||
# Shared callers retain the original event handling, not Mici's slot filter.
|
||||
panel = scroll_panel2.GuiScrollPanel2()
|
||||
gui_app._mouse_events = [event(slot=1, pressed=True)]
|
||||
panel.update(rl.Rectangle(0, 0, 536, 240), 1000)
|
||||
assert panel.state == scroll_panel2.ScrollState.PRESSED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["wifi", "bluetooth"])
|
||||
def test_nested_forget_button_rejects_swipe_and_accepts_next_tap(make_scroller, monkeypatch, kind):
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.bluetooth import BluetoothDeviceButton
|
||||
|
||||
cls = WifiButton if kind == "wifi" else BluetoothDeviceButton
|
||||
button = object.__new__(cls)
|
||||
widgets.Widget.__init__(button)
|
||||
button.set_rect(rl.Rectangle(0, 0, 402, 180))
|
||||
button._grow_animation_until = None
|
||||
button._shake_start = None
|
||||
button._forget_btn = Item()
|
||||
# Exercise the real callback forwarding and touch dispatch without network
|
||||
# services or drawing; use the full card as the nested button's hit area.
|
||||
monkeypatch.setattr(button, "_update_state", lambda: None)
|
||||
monkeypatch.setattr(button, "_render", lambda rect: button._forget_btn.render(rect))
|
||||
monkeypatch.setattr(button, "_handle_mouse_release", lambda pos: None)
|
||||
scroller = make_scroller(items=[Item(), button] + [Item() for _ in range(6)])
|
||||
batch(scroller, event(pressed=True))
|
||||
batch(scroller, event(180, t=1.01), event(160, t=1.21), event(160, released=True, t=1.22))
|
||||
assert button._forget_btn.clicks == 0
|
||||
|
||||
batch(scroller, event(160, pressed=True, t=1.30), event(160, released=True, t=1.35))
|
||||
assert button._forget_btn.clicks == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tap_after_drag", [False, True])
|
||||
def test_option_picker_uses_event_cancellation(make_scroller, monkeypatch, tap_after_drag):
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
|
||||
scroller = make_scroller()
|
||||
dialog = object.__new__(BigMultiOptionDialog)
|
||||
widgets.Widget.__init__(dialog)
|
||||
dialog.set_rect(scroller.rect)
|
||||
dialog._scroll_inner = scroller
|
||||
dialog._selected_option = "option"
|
||||
scroller.items[0].option = "option"
|
||||
selections = []
|
||||
monkeypatch.setattr(dialog, "_on_option_selected", selections.append)
|
||||
monkeypatch.setattr(NavWidget, "_handle_mouse_event", lambda *args: None)
|
||||
gui_app._mouse_events = [event(pressed=True)]
|
||||
scroller.render()
|
||||
dialog._process_mouse_events()
|
||||
gui_app._mouse_events = [event(180, t=1.01), event(160, t=1.21), event(160, released=True, t=1.22)]
|
||||
if tap_after_drag:
|
||||
gui_app._mouse_events += [event(160, pressed=True, t=1.30), event(160, released=True, t=1.35)]
|
||||
scroller.render()
|
||||
dialog._process_mouse_events()
|
||||
|
||||
assert selections == (["option"] if tap_after_drag else [])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("guard", ["disabled", "moving", "original_callback", "original_event_callback"])
|
||||
def test_existing_item_guards_still_reject_taps(make_scroller, monkeypatch, guard):
|
||||
items = [Item() for _ in range(8)]
|
||||
if guard == "original_callback":
|
||||
items[1].set_touch_valid_callback(lambda: False)
|
||||
elif guard == "original_event_callback":
|
||||
items[1].set_touch_event_valid_callback(lambda ev: False)
|
||||
scroller = make_scroller(items=items)
|
||||
if guard == "disabled":
|
||||
items[1].set_enabled(False)
|
||||
elif guard == "moving":
|
||||
monkeypatch.setattr(rl, "draw_rectangle_rec", lambda *args: None)
|
||||
scroller.move_item(1, 2)
|
||||
batch(scroller, event(pressed=True), event(released=True, t=1.05))
|
||||
|
||||
assert sum(item.clicks for item in items) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("horizontal", [True, False])
|
||||
def test_nav_scroller_swipe_to_dismiss_still_works(make_scroller, horizontal):
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller, NavRawScrollPanel
|
||||
|
||||
class RawPanel(NavRawScrollPanel):
|
||||
def _render(self, _):
|
||||
pass
|
||||
|
||||
nav = NavScroller() if horizontal else RawPanel()
|
||||
nav.set_rect(rl.Rectangle(0, 0, 536, 240))
|
||||
if horizontal:
|
||||
nav._scroller._show_scroll_indicator = False
|
||||
nav._scroller._edge_shadows = False
|
||||
nav._scroller.add_widgets([Item() for _ in range(8)])
|
||||
for ev in (event(y=50, pressed=True), event(y=150, t=1.1), event(y=150, released=True, t=1.15)):
|
||||
gui_app._mouse_events = [ev]
|
||||
# Same child-before-parent dispatch order as rendering, without drawing.
|
||||
if horizontal:
|
||||
nav._scroller.render(nav.rect)
|
||||
else:
|
||||
nav._scroll_panel.update(nav.rect, 1000)
|
||||
nav._process_mouse_events()
|
||||
|
||||
assert nav._playing_dismiss_animation
|
||||
if horizontal:
|
||||
assert sum(item.clicks for item in nav._scroller.items) == 0
|
||||
|
||||
@@ -39,6 +39,7 @@ class Widget(abc.ABC):
|
||||
# if current mouse/touch down started within the widget's rectangle
|
||||
self.__tracking_is_pressed = [False] * MAX_TOUCH_SLOTS
|
||||
self._touch_valid_callback: Callable[[], bool] | None = None
|
||||
self._touch_event_valid_callback: Callable[[MouseEvent], bool] | None = None
|
||||
self._click_delay: float | None = None # seconds to hold is_pressed after release
|
||||
self._click_release_time: float | None = None
|
||||
self._click_callback: Callable[[], None] | None = None
|
||||
@@ -91,6 +92,10 @@ class Widget(abc.ABC):
|
||||
"""Check if the widget can be touched."""
|
||||
return self._touch_valid_callback() if self._touch_valid_callback else True
|
||||
|
||||
def set_touch_event_valid_callback(self, touch_callback: Callable[[MouseEvent], bool]) -> None:
|
||||
"""Validate individual events when a parent has already processed the touch batch."""
|
||||
self._touch_event_valid_callback = touch_callback
|
||||
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
changed = (self._rect.x != x or self._rect.y != y)
|
||||
self._rect = rl.Rectangle(x, y, self._rect.width, self._rect.height)
|
||||
@@ -146,10 +151,11 @@ class Widget(abc.ABC):
|
||||
if not self._multi_touch and mouse_event.slot != 0:
|
||||
continue
|
||||
|
||||
event_touch_valid = touch_valid and (self._touch_event_valid_callback is None or self._touch_event_valid_callback(mouse_event))
|
||||
mouse_in_rect = rl.check_collision_point_rec(mouse_event.pos, hit_rect)
|
||||
# Ignores touches/presses that start outside our rect
|
||||
# Allows touch to leave the rect and come back in focus if mouse did not release
|
||||
if mouse_event.left_pressed and touch_valid:
|
||||
if mouse_event.left_pressed and event_touch_valid:
|
||||
if mouse_in_rect:
|
||||
self._handle_mouse_press(mouse_event.pos)
|
||||
self.__is_pressed[mouse_event.slot] = True
|
||||
@@ -157,7 +163,7 @@ class Widget(abc.ABC):
|
||||
self._handle_mouse_event(mouse_event)
|
||||
|
||||
# Callback such as scroll panel signifies user is scrolling
|
||||
elif not touch_valid:
|
||||
elif not event_touch_valid:
|
||||
self.__is_pressed[mouse_event.slot] = False
|
||||
self.__tracking_is_pressed[mouse_event.slot] = False
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ from collections.abc import Callable
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter, BounceFilter
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState
|
||||
from openpilot.system.ui.lib.application import gui_app, MouseEvent
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState, MIN_DRAG_PIXELS
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
|
||||
@@ -25,6 +25,63 @@ DO_ZOOM = False
|
||||
DO_JELLO = False
|
||||
|
||||
|
||||
class _MiciScrollPanel(GuiScrollPanel2):
|
||||
"""Mici gesture ownership, using the shared scrolling physics unchanged."""
|
||||
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True):
|
||||
super().__init__(horizontal, handle_out_of_bounds)
|
||||
self._event_touch_valid: dict[MouseEvent, bool] = {}
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Forget touch history without changing the scroll position."""
|
||||
self._state = ScrollState.STEADY
|
||||
self._velocity = 0.0
|
||||
self._velocity_buffer.clear()
|
||||
self._initial_click_event = None
|
||||
self._previous_mouse_event = None
|
||||
self._snap_target = None
|
||||
self._touch_active = False
|
||||
self.touch_started = False
|
||||
self._event_touch_valid.clear()
|
||||
|
||||
def is_event_touch_valid(self, event: MouseEvent) -> bool:
|
||||
return self._event_touch_valid.get(event, False)
|
||||
|
||||
def _process_mouse_events(self, bounds: rl.Rectangle, bounds_size: float, content_size: float) -> None:
|
||||
self._event_touch_valid.clear()
|
||||
self.touch_started = False
|
||||
if not self.enabled:
|
||||
self.reset()
|
||||
# Disabling scrolling alone must not disable the controls inside it.
|
||||
self._event_touch_valid.update((event, True) for event in gui_app.mouse_events if event.slot == 0)
|
||||
return
|
||||
|
||||
for event in gui_app.mouse_events:
|
||||
if event.slot != 0:
|
||||
continue
|
||||
if event.left_pressed:
|
||||
if not rl.check_collision_point_rec(event.pos, bounds):
|
||||
continue
|
||||
self._touch_active = True
|
||||
self.touch_started = True
|
||||
if not self._touch_active:
|
||||
continue
|
||||
|
||||
was_dragging = self._state == ScrollState.MANUAL_SCROLL
|
||||
if self._state == ScrollState.PRESSED and self._initial_click_event is not None:
|
||||
was_dragging |= abs(self._get_mouse_pos(event) - self._get_mouse_pos(self._initial_click_event)) > MIN_DRAG_PIXELS
|
||||
super()._handle_mouse_event(event, bounds, bounds_size, content_size)
|
||||
# Preserve drag cancellation through release, even if another tap follows
|
||||
# in this batch. Children consume these events after the panel updates.
|
||||
self._event_touch_valid[event] = not (was_dragging or self._state == ScrollState.MANUAL_SCROLL)
|
||||
self._previous_mouse_event = event
|
||||
if event.left_released:
|
||||
self._touch_active = False
|
||||
if self._state == ScrollState.MANUAL_SCROLL:
|
||||
# A release can itself cross the drag threshold. Finish that drag now.
|
||||
super()._handle_mouse_event(event, bounds, bounds_size, content_size)
|
||||
|
||||
|
||||
class ScrollIndicator(Widget):
|
||||
HORIZONTAL_MARGIN = 4
|
||||
|
||||
@@ -95,7 +152,7 @@ class _Scroller(Widget):
|
||||
# when not pressed, snap to closest item to be center
|
||||
self._scroll_snap_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items)
|
||||
self.scroll_panel = _MiciScrollPanel(self._horizontal, handle_out_of_bounds=not self._snap_items)
|
||||
self._scroll_enabled: bool | Callable[[], bool] = True
|
||||
|
||||
self._show_scroll_indicator = scroll_indicator and self._horizontal
|
||||
@@ -148,9 +205,12 @@ class _Scroller(Widget):
|
||||
|
||||
# preserve original touch valid callback
|
||||
original_touch_valid_callback = item._touch_valid_callback
|
||||
item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled and not self._scrolling_to[1]
|
||||
item.set_touch_valid_callback(lambda: self.enabled and not self._scrolling_to[1]
|
||||
and not self.moving_items and (original_touch_valid_callback() if
|
||||
original_touch_valid_callback else True))
|
||||
original_event_callback = item._touch_event_valid_callback
|
||||
item.set_touch_event_valid_callback(lambda event: self.scroll_panel.is_event_touch_valid(event) and
|
||||
(original_event_callback(event) if original_event_callback else True))
|
||||
|
||||
def add_widgets(self, items: list[Widget]) -> None:
|
||||
for item in items:
|
||||
@@ -189,6 +249,8 @@ class _Scroller(Widget):
|
||||
scroll_enabled = self._scroll_enabled() if callable(self._scroll_enabled) else self._scroll_enabled
|
||||
self.scroll_panel.set_enabled(scroll_enabled and self.enabled and not self._scrolling_to[1])
|
||||
self.scroll_panel.update(self._rect, content_size)
|
||||
if self.scroll_panel.touch_started and not self._scrolling_to[1]:
|
||||
self._scrolling_to = None, False
|
||||
if not self._snap_items:
|
||||
return self.scroll_panel.get_offset()
|
||||
|
||||
@@ -398,6 +460,7 @@ class _Scroller(Widget):
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self.scroll_panel.reset()
|
||||
for item in self._items:
|
||||
item.show_event()
|
||||
|
||||
@@ -414,6 +477,7 @@ class _Scroller(Widget):
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self.scroll_panel.reset()
|
||||
for item in self._items:
|
||||
item.hide_event()
|
||||
|
||||
@@ -450,12 +514,17 @@ class NavRawScrollPanel(NavWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
||||
self._scroll_panel = _MiciScrollPanel(horizontal=False)
|
||||
self._scroll_panel.set_enabled(lambda: self.enabled and not self.is_dismissing)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroll_panel.reset()
|
||||
self._scroll_panel.set_offset(0)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._scroll_panel.reset()
|
||||
|
||||
def _back_enabled(self) -> bool:
|
||||
return self._scroll_panel.get_offset() >= -20
|
||||
|
||||
Reference in New Issue
Block a user