Skip to content

Commit ea51d1f

Browse files
authored
Merge pull request #393 from ppizarror/menu_border_color
Menu border color
2 parents f3b96d9 + 85eea41 commit ea51d1f

File tree

8 files changed

+151
-5
lines changed

8 files changed

+151
-5
lines changed

pygame_menu/_scrollarea.py

+94-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import pygame
2020
import pygame_menu
2121

22+
from itertools import product
2223
from pygame_menu._base import Base
2324
from pygame_menu._decorator import Decorator
2425
from pygame_menu.locals import POSITION_SOUTHEAST, POSITION_SOUTHWEST, POSITION_WEST, \
@@ -91,6 +92,8 @@ class ScrollArea(Base):
9192
:param area_width: Width of scrollable area in px
9293
:param area_height: Height of scrollable area in px
9394
:param area_color: Background color, it can be a color or an image
95+
:param border_color: Border color
96+
:param border_width: Border width in px
9497
:param controls_joystick: Use joystick events
9598
:param controls_keyboard: Use keyboard events
9699
:param controls_mouse: Use mouse events
@@ -114,6 +117,10 @@ class ScrollArea(Base):
114117
:param world: Surface to draw and scroll
115118
"""
116119
_area_color: Optional[Union[ColorInputType, 'pygame_menu.BaseImage']]
120+
_border_color: Optional[Union[ColorInputType, 'pygame_menu.BaseImage']]
121+
_border_tiles: List['pygame.Surface']
122+
_border_tiles_size: Tuple2IntType
123+
_border_width: int
117124
_bg_surface: Optional['pygame.Surface']
118125
_decorator: 'Decorator'
119126
_extend_x: int
@@ -135,6 +142,8 @@ def __init__(
135142
area_width: int,
136143
area_height: int,
137144
area_color: Optional[Union[ColorInputType, 'pygame_menu.BaseImage']] = None,
145+
border_color: Optional[Union[ColorInputType, 'pygame_menu.BaseImage']] = None,
146+
border_width: int = 0,
138147
controls_joystick: bool = True,
139148
controls_keyboard: bool = True,
140149
controls_mouse: bool = True,
@@ -161,6 +170,7 @@ def __init__(
161170

162171
assert isinstance(area_height, int)
163172
assert isinstance(area_width, int)
173+
assert isinstance(border_width, int)
164174
assert isinstance(controls_joystick, bool)
165175
assert isinstance(controls_keyboard, bool)
166176
assert isinstance(controls_mouse, bool)
@@ -175,6 +185,18 @@ def __init__(
175185

176186
if area_color is not None and not isinstance(area_color, pygame_menu.BaseImage):
177187
area_color = assert_color(area_color)
188+
if border_color is not None and not isinstance(border_color, pygame_menu.BaseImage):
189+
border_color = assert_color(border_color)
190+
191+
# Create tiles
192+
if isinstance(border_color, pygame_menu.BaseImage):
193+
iw, ih = border_color.get_size()
194+
tw, th = iw // 3, ih // 3
195+
self._border_tiles_size = tw, th
196+
self._border_tiles = [
197+
border_color.subsurface((x, y, tw, th))
198+
for x, y in product(range(0, iw, tw), range(0, ih, th))
199+
]
178200

179201
scrollbar_color = assert_color(scrollbar_color)
180202
scrollbar_slider_color = assert_color(scrollbar_slider_color)
@@ -195,7 +217,8 @@ def __init__(
195217
unique_scrolls.append(s)
196218

197219
self._area_color = area_color
198-
self._bg_surface = None
220+
self._border_color = border_color
221+
self._border_width = border_width
199222
self._bg_surface = None
200223
self._decorator = Decorator(self)
201224
self._scrollbar_positions = tuple(unique_scrolls) # Ensure unique
@@ -300,10 +323,11 @@ def _make_background_surface(self) -> None:
300323
# Make surface
301324
self._bg_surface = make_surface(width=self._rect.width + self._extend_x,
302325
height=self._rect.height + self._extend_y)
326+
rect = self._bg_surface.get_rect()
303327
if self._area_color is not None:
304328
if isinstance(self._area_color, pygame_menu.BaseImage):
305329
self._area_color.draw(surface=self._bg_surface,
306-
area=self._bg_surface.get_rect())
330+
area=rect)
307331
else:
308332
self._bg_surface.fill(assert_color(self._area_color))
309333

@@ -476,6 +500,74 @@ def draw(self, surface: 'pygame.Surface') -> 'ScrollArea':
476500
# Draw post decorator
477501
self._decorator.draw_post(surface)
478502

503+
# Create border
504+
if isinstance(self._border_color, pygame_menu.BaseImage): # Image
505+
tw, th = self._border_tiles_size
506+
border_rect = pygame.Rect(
507+
int(self._rect.x - tw),
508+
int(self._rect.y - th),
509+
int(self._rect.width + 2 * tw),
510+
int(self._rect.height + 2 * th)
511+
)
512+
513+
surface_blit = surface.blit
514+
(
515+
tile_nw,
516+
tile_w,
517+
tile_sw,
518+
tile_n,
519+
tile_c,
520+
tile_s,
521+
tile_ne,
522+
tile_e,
523+
tile_se,
524+
) = self._border_tiles
525+
left, top = self._rect.topleft
526+
left -= tw
527+
top -= th
528+
529+
# draw top and bottom tiles
530+
area: Optional[Tuple[int, int, int, int]]
531+
532+
for x in range(border_rect.left, border_rect.right, tw):
533+
if x + tw >= border_rect.right:
534+
area = 0, 0, tw - (x + border_rect.right), th
535+
else:
536+
area = None
537+
surface_blit(tile_n, (x, top), area)
538+
surface_blit(tile_s, (x, border_rect.bottom - th), area)
539+
540+
# draw left and right tiles
541+
for y in range(border_rect.top, border_rect.bottom, th):
542+
if y + th >= border_rect.bottom:
543+
area = 0, 0, tw, th - (y + border_rect.bottom)
544+
else:
545+
area = None
546+
surface_blit(tile_w, (left, y), area)
547+
surface_blit(tile_e, (border_rect.right - tw, y), area)
548+
549+
# draw corners
550+
surface_blit(tile_nw, (left, top))
551+
surface_blit(tile_sw, (left, border_rect.bottom - th))
552+
surface_blit(tile_ne, (border_rect.right - tw, top))
553+
surface_blit(tile_se, (border_rect.right - tw, border_rect.bottom - th))
554+
555+
else: # Color
556+
if self._border_width == 0 or self._border_color is None:
557+
return self
558+
border_rect = pygame.Rect(
559+
int(self._rect.x - self._border_width),
560+
int(self._rect.y - self._border_width),
561+
int(self._rect.width + 2 * self._border_width),
562+
int(self._rect.height + 2 * self._border_width)
563+
)
564+
pygame.draw.rect(
565+
surface,
566+
self._border_color,
567+
border_rect,
568+
self._border_width
569+
)
570+
479571
return self
480572

481573
def get_hidden_width(self) -> int:

pygame_menu/baseimage.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'IMAGE_EXAMPLE_METAL',
1919
'IMAGE_EXAMPLE_PYGAME_MENU',
2020
'IMAGE_EXAMPLE_PYTHON',
21+
'IMAGE_EXAMPLE_TILED_BORDER',
2122
'IMAGE_EXAMPLE_WALLPAPER',
2223
'IMAGE_EXAMPLES',
2324

@@ -58,11 +59,12 @@
5859
IMAGE_EXAMPLE_METAL = __images_path__.format('metal.png')
5960
IMAGE_EXAMPLE_PYGAME_MENU = __images_path__.format('pygame_menu.png')
6061
IMAGE_EXAMPLE_PYTHON = __images_path__.format('python.svg')
62+
IMAGE_EXAMPLE_TILED_BORDER = __images_path__.format('tiled_border.png')
6163
IMAGE_EXAMPLE_WALLPAPER = __images_path__.format('wallpaper.jpg')
6264

6365
IMAGE_EXAMPLES = (IMAGE_EXAMPLE_CARBON_FIBER, IMAGE_EXAMPLE_GRAY_LINES,
6466
IMAGE_EXAMPLE_METAL, IMAGE_EXAMPLE_PYGAME_MENU, IMAGE_EXAMPLE_PYTHON,
65-
IMAGE_EXAMPLE_WALLPAPER)
67+
IMAGE_EXAMPLE_TILED_BORDER, IMAGE_EXAMPLE_WALLPAPER)
6668

6769
# Drawing modes
6870
IMAGE_MODE_CENTER = 100
@@ -404,6 +406,15 @@ def get_height(self) -> int:
404406
"""
405407
return int(self._surface.get_height())
406408

409+
def subsurface(self, rect: Union[Tuple4IntType, 'pygame.Rect']) -> 'pygame.Surface':
410+
"""
411+
Return a subsurface from a rect.
412+
413+
:param rect: Rect
414+
:return: Subsurface
415+
"""
416+
return self._surface.subsurface(rect)
417+
407418
def get_size(self) -> Tuple2IntType:
408419
"""
409420
Return the size in pixels of the image.

pygame_menu/menu.py

+2
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,8 @@ def __init__(
541541
area_color=self._theme.background_color,
542542
area_height=self._height - extend_y,
543543
area_width=self._width,
544+
border_color=self._theme.border_color,
545+
border_width=self._theme.border_width,
544546
controls_joystick=self._joystick,
545547
controls_keyboard=self._keyboard,
546548
controls_mouse=self._mouse,
176 Bytes
Loading

pygame_menu/themes.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ class Theme(object):
8080
width/height see Menu parameters.
8181
8282
:param background_color: Menu background color
83-
:type background_color: tuple, list, :py:class:`pygame_menu.baseimage.BaseImage`
83+
:type background_color: tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`
84+
:param border_color: Menu border color. If border is an image, it will be split in 9 tiles to use top, left, bottom, right, and the corners
85+
:type border_color: tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`, None
86+
:param border_width: Border width in px. Used only if ``border_color`` is not an image
87+
:type border_width: int
8488
:param cursor_color: Cursor color (used in some text-gathering widgets like ``TextInput``)
8589
:type cursor_color: tuple, list, str, int, :py:class:`pygame.Color`
8690
:param cursor_selection_color: Color of the text selection if the cursor is enabled on certain widgets
@@ -238,6 +242,7 @@ class Theme(object):
238242
"""
239243
_disable_validation: bool
240244
background_color: Union[ColorType, 'BaseImage']
245+
border_color: Union[ColorType, 'BaseImage']
241246
cursor_color: ColorType
242247
cursor_selection_color: ColorType
243248
cursor_switch_ms: NumberType
@@ -320,6 +325,8 @@ def __init__(self, **kwargs) -> None:
320325

321326
# Menu general
322327
self.background_color = self._get(kwargs, 'background_color', 'color_image', (220, 220, 220))
328+
self.border_color = self._get(kwargs, 'border_color', 'color_image_none')
329+
self.border_width = self._get(kwargs, 'border_width', int, 0)
323330
self.focus_background_color = self._get(kwargs, 'focus_background_color', 'color', (0, 0, 0, 180))
324331
self.fps = self._get(kwargs, 'fps', NumberInstance, 30)
325332
self.readonly_color = self._get(kwargs, 'readonly_color', 'color', (120, 120, 120))
@@ -463,6 +470,7 @@ def validate(self) -> 'Theme':
463470
if self.widget_selection_effect is None:
464471
self.widget_selection_effect = NoneSelection()
465472

473+
assert isinstance(self.border_width, int)
466474
assert isinstance(self.cursor_switch_ms, NumberInstance)
467475
assert isinstance(self.fps, NumberInstance)
468476
assert isinstance(self.scrollbar_shadow_offset, int)
@@ -489,6 +497,7 @@ def validate(self) -> 'Theme':
489497
# Format colors, this converts all color lists to tuples automatically,
490498
# if it is an image, return the same object
491499
self.background_color = self._format_color_opacity(self.background_color)
500+
self.border_color = self._format_color_opacity(self.border_color, none=True)
492501
self.cursor_color = self._format_color_opacity(self.cursor_color)
493502
self.cursor_selection_color = self._format_color_opacity(self.cursor_selection_color)
494503
self.focus_background_color = self._format_color_opacity(self.focus_background_color)
@@ -538,6 +547,7 @@ def validate(self) -> 'Theme':
538547
self.widget_offset = self._vec_to_tuple(self.widget_offset, 2, NumberInstance)
539548

540549
# Check sizes
550+
assert self.border_width >= 0, 'border width must be equal or greater than zero'
541551
assert self.scrollarea_outer_margin[0] >= 0 and self.scrollarea_outer_margin[1] >= 0, \
542552
'scroll area outer margin must be equal or greater than zero on both axis'
543553
assert self.widget_offset[0] >= 0 and self.widget_offset[1] >= 0, \

pygame_menu/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ def __str__(self) -> str:
3434
patch = property(lambda self: self[2])
3535

3636

37-
vernum = Version(4, 2, 5)
37+
vernum = Version(4, 2, 6)
3838
ver = str(vernum)
3939
rev = ''

test/test_baseimage.py

+8
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,11 @@ def test_cache(self) -> None:
422422
image.draw(surface, r)
423423
self.assertNotEqual(image._last_transform[2], s)
424424
self.assertEqual(image._last_transform[0], 300)
425+
426+
def test_subsurface(self) -> None:
427+
"""
428+
Test subsurface.
429+
"""
430+
image = pygame_menu.BaseImage(pygame_menu.baseimage.IMAGE_EXAMPLE_TILED_BORDER)
431+
self.assertEqual(image.get_size(), (18, 18))
432+
self.assertEqual(image.subsurface((0, 0, 3, 3)).get_size(), (3, 3))

test/test_menu.py

+23
Original file line numberDiff line numberDiff line change
@@ -2395,3 +2395,26 @@ def _resize():
23952395
menu.resize(400, 400, position=(50, 50, False))
23962396
self.assertFalse(menu._position_relative)
23972397
self.assertEqual(menu._position, (50, 50))
2398+
2399+
def test_border_color(self) -> None:
2400+
"""
2401+
Test menu border color.
2402+
"""
2403+
theme = pygame_menu.themes.THEME_DEFAULT.copy()
2404+
self.assertIsNone(theme.border_color)
2405+
theme.border_width = 10
2406+
theme.title_font_size = 15
2407+
2408+
# Test invalid
2409+
theme.border_color = 'invalid'
2410+
self.assertRaises(ValueError, lambda: pygame_menu.Menu('Menu with border color', 250, 250, theme=theme))
2411+
2412+
# Test with border color
2413+
theme.border_color = 'red'
2414+
menu = pygame_menu.Menu('Menu with border color', 250, 250, theme=theme)
2415+
menu.draw(surface)
2416+
2417+
# Test with image
2418+
theme.border_color = pygame_menu.BaseImage(pygame_menu.baseimage.IMAGE_EXAMPLE_TILED_BORDER)
2419+
menu = pygame_menu.Menu('Menu with border image', 250, 250, theme=theme)
2420+
menu.draw(surface)

0 commit comments

Comments
 (0)