Skip to content

Commit 8f2eea5

Browse files
authored
Merge pull request #493 from JaskRendix/patch-1
ScrollBar (at_bottom, at_top)
2 parents 2f0cec4 + b67de1f commit 8f2eea5

File tree

2 files changed

+289
-64
lines changed

2 files changed

+289
-64
lines changed

pygame_menu/widgets/widget/scrollbar.py

+120-62
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ class ScrollBar(Widget):
6868
_values_range: List[NumberType]
6969
_visible_force: int # -1: not set, 0: hidden, 1: shown
7070
scrolling: bool
71+
_at_bottom: bool
72+
_at_top: bool
7173

7274
def __init__(
7375
self,
@@ -133,6 +135,8 @@ def __init__(
133135
# Page step
134136
self._page_step = 0
135137
self._single_step = 20
138+
self._at_bottom = False
139+
self._at_top = True
136140

137141
if values_range[1] - values_range[0] > length:
138142
self.set_page_step(length)
@@ -174,21 +178,14 @@ def flip(self, *args, **kwargs) -> 'ScrollBar':
174178
raise WidgetTransformationNotImplemented()
175179

176180
def _apply_size_changes(self) -> None:
177-
"""
178-
Apply scrollbar changes.
179-
"""
181+
"""Apply scrollbar changes."""
180182
opp_orientation = 1 if self._orientation == 0 else 0 # Opposite of orientation
181183
dims = ('width', 'height')
182184
setattr(self._rect, dims[self._orientation], int(self._page_ctrl_length))
183185
setattr(self._rect, dims[opp_orientation], self._page_ctrl_thick)
184-
self._slider_rect = pygame.Rect(0, 0, int(self._rect.width), int(self._rect.height))
185-
setattr(self._slider_rect, dims[self._orientation], int(self._page_step))
186-
setattr(self._slider_rect, dims[opp_orientation], self._page_ctrl_thick)
187-
188-
# Update slider position according to the current one
189-
pos = ('x', 'y')
190-
setattr(self._slider_rect, pos[self._orientation], int(self._slider_position))
191-
self._slider_rect = self._slider_rect.inflate(-2 * self._slider_pad, -2 * self._slider_pad)
186+
if self._slider_rect is None:
187+
self._slider_rect = pygame.Rect(0, 0, int(self._rect.width), int(self._rect.height))
188+
self._update_slider_rect()
192189

193190
def set_shadow(
194191
self,
@@ -311,17 +308,8 @@ def _render(self) -> Optional[bool]:
311308
slider_color = self._slider_color if not self.readonly else self._font_readonly_color
312309
mouse_hover = (self.scrolling and self._clicked) or self._mouseover
313310
slider_color = self._slider_hover_color if mouse_hover else slider_color
314-
if self._shadow_enabled:
315-
lit_rect = pygame.Rect(self._slider_rect)
316-
slider_rect = lit_rect.inflate(-self._shadow_offset * 2, -self._shadow_offset * 2)
317-
shadow_rect = lit_rect.inflate(-self._shadow_offset, -self._shadow_offset)
318-
shadow_rect = shadow_rect.move(int(self._shadow_tuple[0] / 2), int(self._shadow_tuple[1] / 2))
319-
320-
pygame.draw.rect(self._surface, self._font_selected_color, lit_rect)
321-
pygame.draw.rect(self._surface, self._shadow_color, shadow_rect)
322-
pygame.draw.rect(self._surface, slider_color, slider_rect)
323-
else:
324-
pygame.draw.rect(self._surface, slider_color, self._slider_rect)
311+
self._render_shadow(self._surface, slider_color)
312+
return True
325313

326314
def _scroll(self, rect: 'pygame.Rect', pixels: NumberType) -> bool:
327315
"""
@@ -351,6 +339,7 @@ def _scroll(self, rect: 'pygame.Rect', pixels: NumberType) -> bool:
351339
move_pos[axis] = move
352340
self._slider_rect.move_ip(*move_pos)
353341
self._slider_position += move
342+
self._update_slider_position_flags()
354343
return True
355344

356345
def set_length(self, value: NumberType) -> None:
@@ -508,6 +497,107 @@ def get_slider_rect(self) -> 'pygame.Rect':
508497
"""
509498
return self._slider_rect.move(*self.get_rect(to_absolute_position=True).topleft)
510499

500+
def _update_slider_position_flags(self) -> None:
501+
"""Updates the at_bottom and at_top flags based on slider position."""
502+
max_slider_position = self._page_ctrl_length - self._page_step
503+
self._at_bottom = self._slider_position >= max_slider_position
504+
self._at_top = self._slider_position <= 0
505+
506+
def _update_slider_rect(self) -> None:
507+
"""Updates the slider rect based on position and size."""
508+
opp_orientation = 1 if self._orientation == 0 else 0 # Opposite of orientation
509+
dims = ('width', 'height')
510+
setattr(self._slider_rect, dims[self._orientation], int(self._page_step))
511+
setattr(self._slider_rect, dims[opp_orientation], self._page_ctrl_thick)
512+
# Update slider position according to the current one
513+
pos = ('x', 'y')
514+
setattr(self._slider_rect, pos[self._orientation], int(self._slider_position))
515+
self._slider_rect = self._slider_rect.inflate(-2 * self._slider_pad, -2 * self._slider_pad)
516+
517+
def _set_slider_position(self, position: int) -> None:
518+
"""Sets the slider position and updates related flags."""
519+
self._slider_position = position
520+
self._update_slider_rect()
521+
self._update_slider_position_flags()
522+
self._render()
523+
524+
def is_at_bottom(self) -> bool:
525+
"""Return True if the scrollbar is at the bottom, False otherwise."""
526+
self._update_slider_position_flags()
527+
return self._at_bottom
528+
529+
def is_at_top(self) -> bool:
530+
"""Return True if the scrollbar is at the top, False otherwise."""
531+
self._update_slider_position_flags()
532+
return self._at_top
533+
534+
def bump_to_top(self) -> None:
535+
"""Set the scrollbar to the top."""
536+
self._set_slider_position(0)
537+
538+
def bump_to_bottom(self) -> None:
539+
"""Set the scrollbar to the bottom."""
540+
self._set_slider_position(self._page_ctrl_length - self._page_step)
541+
542+
def _handle_mouse_event(self, event: pygame.event.Event, rect: pygame.Rect) -> bool:
543+
"""Handles mouse related events."""
544+
# Vertical bar: scroll down (4) or up (5). Mouse must be placed
545+
# over the area to enable this feature
546+
if event.button in (4, 5) and self._orientation == 1 and (
547+
self._scrollarea is None or self._scrollarea.mouse_is_over()
548+
):
549+
direction = -1 if event.button == 4 else 1
550+
if self._scroll(rect, direction * self._single_step):
551+
self.change()
552+
return True
553+
# Click button (left, middle, right)
554+
elif event.button in (1, 2, 3):
555+
# The _slider_rect origin is related to the widget surface
556+
if self.get_slider_rect().collidepoint(*event.pos):
557+
self.scrolling = True
558+
self._clicked = True
559+
self._render()
560+
return True
561+
elif rect.collidepoint(*event.pos):
562+
# Moves towards the click by one "page" (= slider length without pad)
563+
s_rect = self.get_slider_rect()
564+
direction = 1 if event.pos[self._orientation] > (s_rect.x if self._orientation == 0 else s_rect.y) else -1
565+
if self._scroll(rect, direction * self._page_step):
566+
self.change()
567+
return True
568+
return False
569+
570+
def _handle_touch_event(self, event: pygame.event.Event, rect: pygame.Rect) -> bool:
571+
"""Handles touchscreen related events."""
572+
pos = get_finger_pos(self._menu, event)
573+
# The _slider_rect origin is related to the widget surface
574+
if self.get_slider_rect().collidepoint(*pos):
575+
self.scrolling = True
576+
self._clicked = True
577+
self._render()
578+
return True
579+
elif rect.collidepoint(*pos):
580+
# Moves towards the click by one "page" (= slider length without pad)
581+
s_rect = self.get_slider_rect()
582+
direction = 1 if pos[self._orientation] > (s_rect.x if self._orientation == 0 else s_rect.y) else -1
583+
if self._scroll(rect, direction * self._page_step):
584+
self.change()
585+
return True
586+
return False
587+
588+
def _render_shadow(self, surface: pygame.Surface, slider_color: ColorType) -> None:
589+
"""Renders the slider shadow if enabled."""
590+
if self._shadow_enabled:
591+
lit_rect = pygame.Rect(self._slider_rect)
592+
slider_rect = lit_rect.inflate(-self._shadow_offset * 2, -self._shadow_offset * 2)
593+
shadow_rect = lit_rect.inflate(-self._shadow_offset, -self._shadow_offset)
594+
shadow_rect = shadow_rect.move(int(self._shadow_tuple[0] / 2), int(self._shadow_tuple[1] / 2))
595+
pygame.draw.rect(surface, self._font_selected_color, lit_rect)
596+
pygame.draw.rect(surface, self._shadow_color, shadow_rect)
597+
pygame.draw.rect(surface, slider_color, slider_rect)
598+
else:
599+
pygame.draw.rect(surface, slider_color, self._slider_rect)
600+
511601
def update(self, events: EventVectorType) -> bool:
512602
self.apply_update_callbacks(events)
513603

@@ -574,6 +664,8 @@ def update(self, events: EventVectorType) -> bool:
574664
# Check scrolling
575665
if self._scroll(rect, rel):
576666
self.change()
667+
self.is_at_bottom()
668+
self.is_at_top()
577669
return True
578670

579671
# Mouse enters or leaves the window
@@ -593,46 +685,12 @@ def update(self, events: EventVectorType) -> bool:
593685
self._scroll(rect, my - lmy)
594686

595687
# User clicks the slider rect
596-
elif (
597-
event.type == pygame.MOUSEBUTTONDOWN and self._mouse_enabled or
598-
event.type == FINGERDOWN and self._touchscreen_enabled and self._menu is not None
599-
):
600-
# Vertical bar: scroll down (4) or up (5). Mouse must be placed
601-
# over the area to enable this feature
602-
if (
603-
not event.type == FINGERDOWN and
604-
event.button in (4, 5) and
605-
self._orientation == 1 and
606-
(
607-
self._scrollarea is not None and self._scrollarea.mouse_is_over() or
608-
self._scrollarea is None
609-
)
610-
):
611-
direction = -1 if event.button == 4 else 1
612-
if self._scroll(rect, direction * self._single_step):
613-
self.change()
614-
return True
615-
616-
# Click button (left, middle, right)
617-
elif event.type == FINGERDOWN or event.button in (1, 2, 3):
618-
event_pos = get_finger_pos(self._menu, event)
619-
620-
# The _slider_rect origin is related to the widget surface
621-
if self.get_slider_rect().collidepoint(*event_pos):
622-
# Initialize scrolling
623-
self.scrolling = True
624-
self._clicked = True
625-
self._render()
626-
return True
627-
628-
elif rect.collidepoint(*event_pos):
629-
# Moves towards the click by one "page" (= slider length without pad)
630-
s_rect = self.get_slider_rect()
631-
pos = (s_rect.x, s_rect.y)
632-
direction = 1 if event_pos[self._orientation] > pos[self._orientation] else -1
633-
if self._scroll(rect, direction * self._page_step):
634-
self.change()
635-
return True
688+
elif event.type == pygame.MOUSEBUTTONDOWN and self._mouse_enabled:
689+
if self._handle_mouse_event(event, rect):
690+
return True
691+
elif event.type == FINGERDOWN and self._touchscreen_enabled and self._menu is not None:
692+
if self._handle_touch_event(event, rect):
693+
return True
636694

637695
# User releases mouse button if scrolling
638696
elif (event.type == pygame.MOUSEBUTTONUP and self._mouse_enabled or

0 commit comments

Comments
 (0)