"""
pygame-menu
https://github.com/ppizarror/pygame-menu
FRAME
Widget container.
"""
__all__ = [
# Main class
'Frame',
'FrameManager',
# Types
'FrameTitleBackgroundColorType',
'FrameTitleButtonType',
# Constants
'FRAME_DEFAULT_TITLE_BACKGROUND_COLOR',
'FRAME_TITLE_BUTTON_CLOSE',
'FRAME_TITLE_BUTTON_MAXIMIZE',
'FRAME_TITLE_BUTTON_MINIMIZE'
]
import pygame
import pygame_menu
from abc import ABC
from pygame_menu._decorator import Decorator
from pygame_menu.baseimage import BaseImage
from pygame_menu.locals import CURSOR_HAND, ORIENTATION_VERTICAL, \
ORIENTATION_HORIZONTAL, ALIGN_CENTER, ALIGN_LEFT, ALIGN_RIGHT, POSITION_CENTER, \
POSITION_NORTH, POSITION_SOUTH, FINGERUP, FINGERDOWN, FINGERMOTION
from pygame_menu.font import FontType, assert_font
from pygame_menu.utils import assert_alignment, make_surface, assert_vector, \
assert_orientation, assert_color, fill_gradient, parse_padding, uuid4, warn, \
get_finger_pos
from pygame_menu.widgets.core.widget import Widget, check_widget_mouseleave, \
WidgetTransformationNotImplemented, AbstractWidgetManager
from pygame_menu.widgets.widget.button import Button
from pygame_menu.widgets.widget.label import Label
from pygame_menu._types import Optional, NumberType, Dict, Tuple, Union, List, \
Vector2NumberType, Literal, Tuple2IntType, NumberInstance, Any, ColorInputType, \
EventVectorType, PaddingType, CallbackType, ColorInputGradientType, \
CursorInputType, VectorInstance
# Constants
FRAME_DEFAULT_TITLE_BACKGROUND_COLOR = ((10, 36, 106), (166, 202, 240), False, True)
FRAME_TITLE_BUTTON_CLOSE = 'close'
FRAME_TITLE_BUTTON_MAXIMIZE = 'maximize'
FRAME_TITLE_BUTTON_MINIMIZE = 'minimize'
S_FINGER_FACTOR = 0.25, 0.25
# Types
FrameTitleBackgroundColorType = Optional[Union[ColorInputType, ColorInputGradientType, BaseImage]]
FrameTitleButtonType = Literal[
FRAME_TITLE_BUTTON_CLOSE,
FRAME_TITLE_BUTTON_MAXIMIZE,
FRAME_TITLE_BUTTON_MINIMIZE
]
# noinspection PyMissingOrEmptyDocstring,PyProtectedMember
[docs]class Frame(Widget):
"""
Frame is a widget container, it can pack many widgets.
All widgets inside have a floating position. Widgets inside are placed using
its margin + width/height. ``(0, 0)`` coordinate is the top-left position in
frame.
Frame modifies ``translation`` of widgets. Thus, if widget has a translation
before it will be removed.
Widget packing currently only supports LEFT, CENTER and RIGHT alignment in
the same line, widgets cannot be packed in two different lines. For such
purpose use several frames. If widget width or height exceeds Frame size
``_FrameSizeException`` will be raised.
.. note::
Frame cannot be selected. Thus, it does not receive any selection effect.
.. note::
Frames should be appended to Menu scrollable frames if it's scrollable,
be careful when removing. Check :py:class:`pygame_menu.widgets.DropSelect`
as a complete example of Frame implementation within another Widgets.
.. note::
Frame only accepts translation and resize transformations.
:param width: Frame width in px
:param height: Frame height in px
:param orientation: Frame orientation (horizontal or vertical). See :py:mod:`pygame_menu.locals`
:param frame_id: ID of the frame
"""
_accepts_scrollarea: bool
_accepts_title: bool
_control_widget: Optional['Widget']
_control_widget_last_pos: Optional[Vector2NumberType]
_draggable: bool
_frame_scrollarea: Optional['pygame_menu._scrollarea.ScrollArea']
_frame_size: Tuple2IntType
_frame_title: Optional['Frame']
_has_frames: bool # True if frame has packed other frames
_has_title: bool
_height: int
_menu_can_be_none_pack: bool
_orientation: str
_pack_margin_warning: bool
_pos: Dict[str, Tuple[int, int]] # Widget positioning
_real_rect: 'pygame.Rect'
_recursive_render: int
_widgets: Dict[str, 'Widget'] # widget
_widgets_props: Dict[str, Tuple[str, str]] # alignment, vertical position
_width: int
first_index: int # First selectable widget index
horizontal: bool
last_index: int # Last selectable widget index
def __init__(
self,
width: NumberType,
height: NumberType,
orientation: str,
frame_id: str = ''
) -> None:
super(Frame, self).__init__(widget_id=frame_id)
assert isinstance(width, NumberInstance)
assert isinstance(height, NumberInstance)
assert width > 0, \
f'width must be greater than zero ({width} received)'
assert height > 0, \
f'height must be greater than zero ({height} received)'
assert_orientation(orientation)
# Internals
self._accepts_scrollarea = True
self._accepts_title = True
self._control_widget = None
self._control_widget_last_pos = None # This checks if menu has updated widget position
self._draggable = False
self._frame_scrollarea = None
self._frame_size = (width, height) # Size of the frame, set in make_scrollarea
self._has_frames = False
self._height = int(height)
self._menu_can_be_none_pack = False
self._orientation = orientation
self._pack_margin_warning = True # Set to False for hiding the pack margin warning
self._pos = {}
self._real_rect = pygame.Rect(0, 0, width, height)
self._recursive_render = 0
self._relax = False # If True ignore sizing
self._widgets = {}
self._widgets_props = {}
self._width = int(width)
# Title
self._frame_title = None
self._has_title = False
# Configure widget public's
self.first_index = -1
self.horizontal = orientation == ORIENTATION_HORIZONTAL
self.is_scrollable = False
self.is_selectable = False
self.last_index = -1
[docs] def set_title(
self,
title: str,
cursor: CursorInputType = None,
background_color: FrameTitleBackgroundColorType = FRAME_DEFAULT_TITLE_BACKGROUND_COLOR,
draggable: bool = False,
padding_inner: PaddingType = 0,
padding_outer: PaddingType = 0,
title_alignment: str = ALIGN_LEFT,
title_buttons_alignment: str = ALIGN_RIGHT,
title_font: Optional[FontType] = None,
title_font_color: Optional[ColorInputType] = None,
title_font_size: Optional[int] = None
) -> 'Frame':
"""
Add a title to the frame.
:param title: New title
:param cursor: Title cursor
:param background_color: Title background color. It can be a Color, a gradient, or an image
:param draggable: If ``True`` the title accepts user drag using the mouse
:param padding_inner: Padding inside the title
:param padding_outer: Padding outside the title (respect to the Frame)
:param title_alignment: Alignment of the title
:param title_buttons_alignment: Alignment of the title buttons (if appended later)
:param title_font: Title font. If ``None`` uses the same as the Frame
:param title_font_color: Title font color. If ``None`` uses the same as the Frame
:param title_font_size: Title font size in px. If ``None`` uses the same as the Frame
:return: Title frame object
"""
assert self.configured, \
f'{self.get_class_id()} must be configured before setting a title'
if not self._accepts_title:
raise _FrameDoNotAcceptTitle(f'{self.get_class_id()} does not accept a title')
self._title = title
# If it has previous title
self.remove_title()
assert isinstance(draggable, bool)
self._draggable = draggable
# Format title font properties
if title_font is None:
title_font = self._font_name
assert_font(title_font)
if title_font_color is None:
title_font_color = self._font_color
title_font_color = assert_color(title_font_color)
if title_font_size is None:
title_font_size = self._font_size
assert isinstance(title_font_size, int) and title_font_size > 0
assert_alignment(title_alignment)
assert_alignment(title_buttons_alignment)
# Check alignment are different
assert title_alignment != title_buttons_alignment, \
'title alignment and buttons alignment must be different'
# Create title widget
title_label = Label(
title=title,
label_id=self._id + '+title+label-' + uuid4(short=True)
)
title_label.set_font(
antialias=self._font_antialias,
background_color=None,
color=title_font_color,
font=title_font,
font_size=title_font_size,
readonly_color=self._font_readonly_color,
readonly_selected_color=self._font_readonly_selected_color,
selected_color=self._font_selected_color
)
title_label.set_tab_size(self._tab_size)
title_label.configured = True
title_label.set_menu(self._menu)
title_label._update__repr___(self)
# Create frame title
pad_outer = parse_padding(padding_outer) # top, right, bottom, left
pad_inner = parse_padding(padding_inner)
self._frame_title = Frame(
width=self.get_width() - (pad_outer[1] + pad_outer[3] + pad_inner[1] + pad_inner[3]),
height=title_label.get_height(),
orientation=ORIENTATION_HORIZONTAL,
frame_id=self._id + '+title-' + uuid4(short=True)
)
self._frame_title._accepts_scrollarea = False
self._frame_title._accepts_title = False
self._frame_title._menu_can_be_none_pack = True
self._frame_title._menu = self._menu
self._frame_title.set_attribute('buttons_alignment', title_buttons_alignment)
self._frame_title.set_attribute('pbottom', pad_outer[2] - pad_inner[2])
self._frame_title.set_cursor(cursor if cursor is not None else self._cursor)
self._frame_title.set_padding(padding_inner)
self._frame_title.set_scrollarea(self._scrollarea)
self._frame_title.translate(pad_outer[3] + pad_inner[3], pad_outer[0] + pad_inner[0])
self._frame_title.set_controls(
joystick=self._joystick_enabled,
mouse=self._mouse_enabled,
touchscreen=self._touchscreen_enabled,
keyboard=self._keyboard_enabled
)
if self._frame is not None:
self._frame_title.set_frame(self._frame)
self._frame_title._update__repr___(self)
# Store constructor
self._frame_title.set_attribute(
'constructor', {
'title': title,
'cursor': cursor,
'background_color': background_color,
'draggable': draggable,
'padding_inner': padding_inner,
'padding_outer': padding_outer,
'title_alignment': title_alignment,
'title_buttons_alignment': title_buttons_alignment,
'title_font': title_font,
'title_font_color': title_font_color,
'title_font_size': title_font_size
}
)
# Create frame title background rect
title_bg = make_surface(
self.get_width(),
title_label.get_height() + pad_outer[0] + pad_outer[2] + pad_inner[0] + pad_inner[2]
)
# Blit frame bgrect if scrollable
if self._frame_scrollarea is not None and self.is_scrollable:
area_color = self._background_color
if isinstance(area_color, BaseImage):
area_color.draw(title_bg, area=title_bg.get_rect())
elif area_color is not None:
title_bg.fill(area_color, rect=title_bg.get_rect())
self._frame_title.get_decorator().add_surface(-title_bg.get_width() / 2, -title_bg.get_height() / 2 + 1, title_bg)
# Set background
is_color = True
try:
assert_color(background_color, warn_if_invalid=False)
except (ValueError, AssertionError):
is_color = False
if isinstance(background_color, pygame_menu.BaseImage) or is_color:
self._frame_title.set_background_color(background_color)
else: # Is gradient
assert isinstance(background_color, tuple)
assert len(background_color) == 4, \
'gradient color type must has 4 components (from color, to color, ' \
'vertical, forward)'
w, h = self._frame_title.get_size()
new_surface = make_surface(w, h)
fill_gradient(
surface=new_surface,
color=background_color[0],
gradient=background_color[1],
vertical=background_color[2],
forward=background_color[3]
)
self._frame_title.get_decorator().add_surface(-w / 2, -h / 2 + 1, new_surface)
# Pack title
self._frame_title.pack(title_label, align=title_alignment)
self._has_title = True
self._render()
self.set_position(self._position[0], self._position[1])
self.force_menu_surface_update()
# Title adds frame to scrollable frames even if not scrollable
self._append_menu_update_frame(self)
return self._frame_title
[docs] def remove_title(self) -> 'Frame':
"""
Remove title from current Frame.
:return: Self reference
"""
if not self._accepts_title:
raise _FrameDoNotAcceptTitle(f'{self.get_class_id()} does not accept a title')
if self._has_title:
self._frame_title = None
self._has_title = False
if not self.is_scrollable:
self._remove_menu_update_frame(self)
self._draggable = False
self._render()
self.force_menu_surface_update()
return self
[docs] def get_title(self) -> str:
if not self._has_title:
# raise ValueError(f'{self.get_class_id()} does not have any title')
return ''
return self._title
[docs] def get_inner_size(self) -> Tuple2IntType:
"""
Return Frame inner size (width, height).
:return: Size tuple in px
"""
return self._width, self._height
def _get_menu_update_frames(self) -> List['pygame_menu.widgets.Frame']:
"""
Return the menu update frames list.
.. warning::
Use with caution.
:return: Frame update list if the menu reference is not ``None``, else, return an empty list
"""
if self._menu is not None:
return self._menu._update_frames
return []
def _sort_menu_update_frames(self) -> None:
"""
Sort the menu update frames (frames which receive updates).
"""
if self._menu is not None:
self._menu._sort_update_frames()
def _append_menu_update_frame(self, frame: 'Frame') -> None:
"""
Append update frame to menu and sort.
:param frame: Frame to append
"""
assert isinstance(frame, Frame)
update_frames = self._get_menu_update_frames()
if frame not in update_frames:
update_frames.append(frame)
self._sort_menu_update_frames()
def _remove_menu_update_frame(self, frame: 'Frame') -> None:
"""
Remove update frame to menu and sort.
:param frame: Frame to append
"""
assert isinstance(frame, Frame)
update_frames = self._get_menu_update_frames()
if frame in update_frames:
update_frames.remove(frame)
[docs] def relax(self, relax: bool = True) -> 'Frame':
"""
Set relax status. If ``True`` Frame ignores sizing checks.
:param relax: Relax status
:return: Self reference
"""
assert isinstance(relax, bool)
self._relax = relax
return self
[docs] def get_max_size(self) -> Tuple2IntType:
"""
Return the max size of the frame.
:return: Max (width, height) in px
"""
if self._frame_scrollarea is not None:
return self._frame_scrollarea.get_size(inner=True)
return self.get_size()
[docs] def get_indices(self) -> Tuple[int, int]:
"""
Return first and last selectable indices tuple.
:return: First, Last widget selectable indices
"""
return self.first_index, self.last_index
[docs] def get_total_packed(self) -> int:
"""
Return the total number of packed widgets.
:return: Number of packed widgets
"""
return len(self._widgets.values())
def select(self, *args, **kwargs) -> 'Frame':
return self
def set_selection_effect(self, *args, **kwargs) -> 'Frame':
pass
def _apply_font(self) -> None:
pass
def _title_height(self) -> int:
"""
Return the title height.
:return: Title height in px
"""
if not self._has_title or self._frame_title is None:
return 0
h = self._frame_title.get_height()
h += self._frame_title.get_translate()[1]
h += self._frame_title.get_attribute('pbottom') # Bottom padding
return h
def _render(self) -> None:
self._rect.height = self._frame_size[1] + self._title_height()
self._rect.width = self._frame_size[0]
def _draw(self, *args, **kwargs) -> None:
pass
def scale(self, *args, **kwargs) -> 'Frame':
raise WidgetTransformationNotImplemented()
def set_max_width(self, *args, **kwargs) -> 'Frame':
raise WidgetTransformationNotImplemented()
def set_max_height(self, *args, **kwargs) -> 'Frame':
raise WidgetTransformationNotImplemented()
def rotate(self, *args, **kwargs) -> 'Frame':
raise WidgetTransformationNotImplemented()
def flip(self, *args, **kwargs) -> 'Frame':
raise WidgetTransformationNotImplemented()
[docs] def get_decorator(self) -> 'Decorator':
"""
Frame decorator belongs to the scrollarea decorator if enabled.
:return: ScrollArea decorator
"""
if self.is_scrollable:
return self._frame_scrollarea.get_decorator()
return self._decorator
[docs] def get_index(self, widget: 'Widget') -> int:
"""
Get index of the given widget within the widget list. Throws
``IndexError`` if widget does not exist.
:param widget: Widget
:return: Index
"""
w = self.get_widgets(unpack_subframes=False)
try:
return w.index(widget)
except ValueError:
raise IndexError(f'{widget.get_class_id()} widget does not exist on {self.get_class_id()}')
[docs] def set_position(self, x: NumberType, y: NumberType) -> 'Frame':
if self._has_title:
pad = self.get_padding() # top, right, bottom, left
tx, ty = self.get_translate()
self._frame_title.set_position(x - pad[3] + tx, y - pad[0] + ty)
for w in self._frame_title.get_widgets():
w._set_position_relative_to_frame()
super(Frame, self).set_position(x, y)
if self.is_scrollable:
self._frame_scrollarea.set_position(self._rect.x, self._rect.y + self._title_height())
return self
[docs] def draw(self, surface: 'pygame.Surface') -> 'Frame':
if not self.is_visible():
return self
selected_widget: Optional['Widget'] = None
# Simple case, no scrollarea
if not self.is_scrollable:
self.last_surface = surface
self._draw_shadow(surface)
self._draw_background_color(surface)
self._decorator.draw_prev(surface)
for widget in self._widgets.values():
if widget.is_selected():
selected_widget = widget
widget.draw(surface)
if selected_widget is not None:
selected_widget.draw_after_if_selected(surface)
self._draw_border(surface)
self._decorator.draw_post(surface)
# Scrollarea
else:
self.last_surface = self._surface
self._surface.fill((255, 255, 255, 0))
self._draw_shadow(self._surface, rect=self._real_rect)
self._draw_background_color(self._surface, rect=self._real_rect)
scrollarea_decorator = self.get_decorator()
scrollarea_decorator.force_cache_update()
scrollarea_decorator.draw_prev(self._surface)
for widget in self._widgets.values():
if widget.is_selected():
selected_widget = widget
widget.draw(self._surface)
if selected_widget is not None:
selected_widget.draw_after_if_selected(self._surface)
self._frame_scrollarea.draw(surface)
self._draw_border(surface)
# If title
if self._has_title:
self._frame_title.draw(surface)
self.apply_draw_callbacks()
return self
def _get_ht(self, widget: 'Widget', a: str) -> int:
"""
Return the horizontal translation for widget.
:param widget: Widget
:param a: Horizontal alignment
:return: Px
"""
w = widget.get_width()
# Some systems introduce 1px margin
if w > (self._width + 1) and not self._relax:
raise _FrameSizeException(
f'{widget.get_class_id()} width ({w}) is greater than {self.get_class_id()}'
f' width ({self._width}), try using widget.set_max_width(...) for '
f'avoiding this issue, or set the widget as floating'
)
if a == ALIGN_CENTER:
return int((self._width - w) / 2)
elif a == ALIGN_RIGHT:
return self._width - w
else: # Alignment left
return 0
def _get_vt(self, widget: 'Widget', v: str) -> int:
"""
Return vertical translation for widget.
:param widget: Widget
:param v: Vertical position
:return: Px
"""
h = widget.get_height()
if h > (self._height + 1) and not self._relax:
raise _FrameSizeException(
f'{widget.get_class_id()} height ({h}) is greater than {self.get_class_id()}'
f' height ({self._height}), try using widget.set_max_height(...) '
f'for avoiding this issue, or set the widget as floating'
)
if v == POSITION_CENTER:
return int((self._height - h) / 2)
elif v == POSITION_SOUTH:
return self._height - h
else: # Position north
return 0
def _update_position_horizontal(self) -> None:
"""
Compute widget position for horizontal orientation.
"""
x_left = 0 # Total added to left
x_right = 0 # Total added to right
w_center = 0
for w in self._widgets.values():
align, v_pos = self._widgets_props[w.get_id()]
if not w.is_visible(check_frame=False) or w.is_floating():
continue
if align == ALIGN_CENTER:
w_center += w.get_width() + w.get_margin()[0]
continue
elif align == ALIGN_LEFT:
x_left += w.get_margin()[0]
self._pos[w.get_id()] = (x_left, self._get_vt(w, v_pos) + w.get_margin()[1])
x_left += w.get_width()
elif align == ALIGN_RIGHT:
x_right -= (w.get_width() + w.get_margin()[0])
self._pos[w.get_id()] = (self._width + x_right, self._get_vt(w, v_pos) + w.get_margin()[1])
dw = x_left - x_right
if dw > self._width and not self._relax:
raise _FrameSizeException(
f'{w.get_class_id()} width ({dw}) exceeds {self.get_class_id()}'
f' width ({self._width}). Set frame._relax=True to ignore this Exception'
)
# Now center widgets
available = self._width - (x_left - x_right)
if w_center > available and not self._relax:
raise _FrameSizeException(
f'cannot place center widgets as required width ({w_center}) is '
f'greater than available ({available}) in {self.get_class_id()}.'
f' Set frame._relax=True to ignore this Exception'
)
x_center = int(self._width / 2 - w_center / 2)
for w in self._widgets.values():
align, v_pos = self._widgets_props[w.get_id()]
if not w.is_visible(check_frame=False) or w.is_floating():
continue
if align == ALIGN_CENTER:
x_center += w.get_margin()[0]
self._pos[w.get_id()] = (x_center, self._get_vt(w, v_pos) + w.get_margin()[1])
x_center += w.get_width()
def _update_position_vertical(self) -> None:
"""
Compute widget position for vertical orientation.
"""
y_top = 0 # Total added to top
y_bottom = 0 # Total added to bottom
w_center = 0
for w in self._widgets.values():
align, v_pos = self._widgets_props[w.get_id()]
if not w.is_visible(check_frame=False) or w.is_floating():
continue
if v_pos == POSITION_CENTER:
w_center += w.get_height() + w.get_margin()[1]
continue
elif v_pos == POSITION_NORTH:
y_top += w.get_margin()[1]
self._pos[w.get_id()] = (self._get_ht(w, align) + w.get_margin()[0], y_top)
y_top += w.get_height()
elif v_pos == POSITION_SOUTH:
y_bottom -= (w.get_height() + w.get_margin()[1])
self._pos[w.get_id()] = (self._get_ht(w, align) + w.get_margin()[0], self._height + y_bottom)
dh = y_top - y_bottom
if dh > self._height and not self._relax:
raise _FrameSizeException(
f'{w.get_class_id()} height ({dh}) exceeds {self.get_class_id()}'
f' height ({self._height}). Set frame._relax=True to ignore '
f'this Exception'
)
# Now center widgets
available = self._height - (y_top - y_bottom)
if w_center > available and not self._relax:
raise _FrameSizeException(
f'cannot place center widgets as required height ({w_center}) is'
f' greater than available ({available}) in {self.get_class_id()}.'
f' Set frame._relax=True to ignore this Exception'
)
y_center = int(self._height / 2 - w_center / 2)
for w in self._widgets.values():
align, v_pos = self._widgets_props[w.get_id()]
if not w.is_visible(check_frame=False) or w.is_floating():
continue
if v_pos == POSITION_CENTER:
y_center += w.get_margin()[1]
self._pos[w.get_id()] = (self._get_ht(w, align) + w.get_margin()[0], y_center)
y_center += w.get_height()
[docs] def update_position(self) -> 'Frame':
"""
Update the position of each widget.
:return: Self reference
"""
if len(self._widgets) == 0:
return self
# Update position based on orientation
if self._orientation == ORIENTATION_HORIZONTAL:
self._update_position_horizontal()
elif self._orientation == ORIENTATION_VERTICAL:
self._update_position_vertical()
# Apply position to each widget
for w in self._widgets.keys():
widget = self._widgets[w]
if not widget.is_visible(check_frame=False):
widget.set_position(0, 0)
continue
if widget.is_floating():
tx = 0
ty = 0
else:
tx, ty = self._pos[w]
ty += self._title_height()
if widget.get_menu() is None: # Widget is only appended to Frame
fx, fy = self.get_position()
margin = widget.get_margin()
padding = widget.get_padding()
widget.set_position(fx + margin[0] + padding[3], fy + padding[0])
if self.is_scrollable and isinstance(widget, Frame) and widget.is_scrollable:
widget.get_scrollarea(inner=True).set_position(tx, ty)
# If scrollarea, subtract this position to each widget
if self._frame_scrollarea is not None:
sx, sy = self._frame_scrollarea.get_position()
tx -= sx
ty -= sy
widget._translate_virtual = (tx, ty) # Translate to scrollarea
# Check if control widget has changed positioning. This fixes centering issues
if self._control_widget is not None:
c_pos = self._control_widget.get_position()
if self._control_widget_last_pos != c_pos:
self._control_widget_last_pos = c_pos
if self._recursive_render <= 100 and self._menu is not None:
self._menu.render()
self._recursive_render += 1
else:
self._recursive_render = 0
# If frame has title
if self._has_title:
self._frame_title.update_position()
return self
[docs] def clear(self) -> Union['Widget', Tuple['Widget', ...]]:
"""
Unpack all widgets within frame.
:return: Removed widgets
"""
unpacked = []
for w in self.get_widgets(unpack_subframes=False):
self.unpack(w)
unpacked.append(w)
return tuple(unpacked)
[docs] def resize(
self,
width: NumberType,
height: NumberType,
max_width: Optional[NumberType] = None,
max_height: Optional[NumberType] = None
) -> 'Frame':
"""
Resize the Frame.
:param width: New width in px. Horizontal padding will be subtracted
:param height: New height in px. Vertical padding will be subtracted
:param max_width: Max frame width if the Frame is scrollable. If ``None`` the same width will be used
:param max_height: Max frame height if the Frame is scrollable. If ``None`` the same height will be used
:return: Self reference
"""
assert isinstance(width, NumberInstance)
assert isinstance(height, NumberInstance)
pad_h = self._padding[1] + self._padding[3]
pad_v = self._padding[0] + self._padding[2]
# Subtract padding
width -= pad_h
height -= pad_v
# Check size
assert width > 0 and height > 0, 'new width and height must be greater than zero'
# Update width/height
if width < self._width or height < self._height:
self.relax()
self._frame_size = (width, height) # Size of the frame, set in make_scrollarea
self._height = int(height)
self._real_rect = pygame.Rect(0, 0, width, height)
self._width = int(width)
# Get previous buttons if it has title
prev_has_title = self._has_title
prev_title_frame = self._frame_title
prev_title_buttons = list(self._frame_title.get_widgets()) if self._has_title else []
if len(prev_title_buttons) >= 1: # Pop label
prev_title_buttons.pop(0)
# Make scrollable if scrollable
if self.is_scrollable:
assert self._frame_scrollarea.has_attribute('constructor'), \
'frame scrollarea does not have the "constructor" attribute. Make ' \
'sure the scrollarea has been created using make_scrollarea() method'
kwargs: Dict[str, Any] = self._frame_scrollarea.get_attribute('constructor')
if max_width is None:
max_width = width
if max_height is None:
max_height = height
self.make_scrollarea(
max_height=max_height,
max_width=max_width,
scrollarea_color=kwargs['scrollarea_color'],
scrollbar_color=kwargs['scrollbar_color'],
scrollbar_cursor=kwargs['scrollbar_cursor'],
scrollbar_shadow=kwargs['scrollbar_shadow'],
scrollbar_shadow_color=kwargs['scrollbar_shadow_color'],
scrollbar_shadow_offset=kwargs['scrollbar_shadow_offset'],
scrollbar_shadow_position=kwargs['scrollbar_shadow_position'],
scrollbar_slider_color=kwargs['scrollbar_slider_color'],
scrollbar_slider_hover_color=kwargs['scrollbar_slider_hover_color'],
scrollbar_slider_pad=kwargs['scrollbar_slider_pad'],
scrollbar_thick=kwargs['scrollbar_thick'],
scrollbars=kwargs['scrollbars']
)
else:
assert max_width is None and max_height is None, \
'if previous Frame is not scrollable (make_scrollarea has been ' \
'called) max_width and max_height must be None'
# If had title, remove and create a new one
if self._has_title:
self.remove_title()
if prev_has_title:
for btn in prev_title_buttons:
prev_title_frame.unpack(btn)
btn.set_margin(0, 0)
btn.set_float(False)
assert prev_title_frame.has_attribute('constructor'), \
'frame title does not have the attribute "constructor". Make sure ' \
'the frame title has been created through set_title() method.'
kwargs = prev_title_frame.get_attribute('constructor')
new_title_frame = self.set_title(
title=kwargs['title'],
cursor=kwargs['cursor'],
background_color=kwargs['background_color'],
draggable=kwargs['draggable'],
padding_inner=kwargs['padding_inner'],
padding_outer=kwargs['padding_outer'],
title_alignment=kwargs['title_alignment'],
title_buttons_alignment=kwargs['title_buttons_alignment'],
title_font=kwargs['title_font'],
title_font_color=kwargs['title_font_color'],
title_font_size=kwargs['title_font_size']
)
# Pack previous buttons
# prev_title_buttons.reverse()
for btn in prev_title_buttons:
align = btn.get_attribute('align', kwargs['title_buttons_alignment'])
margin = btn.get_attribute('margin', (0, 0))
new_title_frame.pack(btn, align=align, margin=margin)
# Force render
self._render()
self.force_menu_surface_update()
return self
[docs] def unfloat(self) -> 'Frame':
"""
Disable float status for each subwidget.
:return: Self reference
"""
for w in self.get_widgets(unpack_subframes_include_frame=True):
w.set_float(False)
if self._menu is not None:
self._menu.render()
self._menu.scroll_to_widget(None)
return self
[docs] def set_frame(self, frame: 'pygame_menu.widgets.Frame') -> 'Frame':
assert self != frame, \
f'{frame.get_class_id()} cannot set itself as a frame'
super(Frame, self).set_frame(frame)
if self._frame_title is not None:
self._frame_title.set_frame(frame)
return self
[docs] def unpack(self, widget: 'Widget') -> 'Frame':
"""
Unpack widget from Frame. If widget does not exist, raises ``ValueError``.
Unpacked widgets adopt a floating position and are moved to the last position
of the widget list of Menu
:param widget: Widget to unpack
:return: Unpacked widget
"""
assert widget != self, 'frame cannot unpack itself'
assert len(self._widgets) > 0, 'frame is empty'
wid = widget.get_id()
if wid not in self._widgets.keys():
raise ValueError(f'{widget.get_class_id()} does not exist in {self.get_class_id()}')
assert widget._frame == self, 'widget frame differs from current'
widget.set_float()
if self._menu is not None:
widget.set_scrollarea(self._menu.get_scrollarea())
widget._frame = None
widget._translate_virtual = (0, 0)
del self._widgets[wid]
try:
del self._pos[wid]
except KeyError:
pass
# Move widget to the last position of widget list
menu_widgets = self._get_menu_widgets()
if widget.get_menu() == self._menu and widget in menu_widgets:
self._menu._validate_frame_widgetmove = False
try:
self._menu.move_widget_index(widget, render=False)
# Assertion error if moving widget (last) to same position (last)
except (ValueError, AssertionError):
pass
if isinstance(widget, Frame):
widgets = widget.get_widgets(unpack_subframes_include_frame=True)
for w in widgets:
if w.get_menu() is None or w not in menu_widgets:
continue
self._menu.move_widget_index(w, render=False)
self._menu._validate_frame_widgetmove = True
# Check if frame contains more frames
self._has_frames = False
for k in self.get_widgets(unpack_subframes=False):
if isinstance(k, Frame):
self._has_frames = True
break
# Update selected
if self._menu is not None:
self._menu.move_widget_index(None, update_selected_index=True)
# Update indices
self.update_indices()
# Render menu
self._menu_render()
if self._control_widget == widget:
self._control_widget = None
self._control_widget_last_pos = None
for w in self.get_widgets():
if w.get_menu() is not None:
self._control_widget = w
self._control_widget_last_pos = self._control_widget.get_position()
break
if widget.is_selected():
widget.scroll_to_widget()
if len(self._widgets) == 0: # Scroll to top
self.scrollv(0)
self.scrollh(0)
if isinstance(widget, Frame):
self._sort_menu_update_frames()
# Update widget leave
check_widget_mouseleave()
return widget
[docs] def pack(
self,
widget: Union['Widget', List['Widget'], Tuple['Widget', ...]],
align: str = ALIGN_LEFT,
vertical_position: str = POSITION_NORTH,
margin: Vector2NumberType = (0, 0)
) -> Union['Widget', List['Widget'], Tuple['Widget', ...], Any]:
"""
Packs widget in the frame line. To pack a widget it has to be already
appended to Menu, and the Menu must be the same as the frame.
Packing is added to the same line, for example if three LEFT widgets are
added:
.. code-block:: python
<frame horizontal>
frame.pack(W1, alignment=ALIGN_LEFT, vertical_position=POSITION_NORTH)
frame.pack(W2, alignment=ALIGN_LEFT, vertical_position=POSITION_CENTER)
frame.pack(W3, alignment=ALIGN_LEFT, vertical_position=POSITION_SOUTH)
----------------
|W1 |
| W2 |
| W3 |
----------------
Another example:
.. code-block:: python
<frame horizontal>
frame.pack(W1, alignment=ALIGN_LEFT)
frame.pack(W2, alignment=ALIGN_CENTER)
frame.pack(W3, alignment=ALIGN_RIGHT)
----------------
|W1 W2 W3|
----------------
.. code-block:: python
<frame vertical>
frame.pack(W1, alignment=ALIGN_LEFT)
frame.pack(W2, alignment=ALIGN_CENTER)
frame.pack(W3, alignment=ALIGN_RIGHT)
--------
|W1 |
| W2 |
| W3|
--------
.. note::
Frame does not consider previous widget margin. For such purpose, use
``margin`` pack parameter.
.. note::
It is recommended to force menu rendering after packing all widgets.
.. note::
Packing applies a virtual translation to the widget, previous translation
is not modified.
.. note::
Widget floating is also considered within frames. If a widget is floating,
it does not add any size to the respective positioning.
:param widget: Widget to be packed
:param align: Widget alignment. See :py:mod:`pygame_menu.locals`
:param vertical_position: Vertical position of the widget within frame. Only valid: north, center, and south. See :py:mod:`pygame_menu.locals`
:param margin: (left, top) margin of added widget in px. It overrides the previous widget margin
:return: Added widget references
"""
assert self._menu is not None or self._menu_can_be_none_pack, \
f'{self.get_class_id()} menu must be set before packing widgets'
if isinstance(widget, VectorInstance):
for w in widget:
self.pack(widget=w, align=align, vertical_position=vertical_position)
return widget
assert isinstance(widget, Widget)
if isinstance(widget, Frame):
assert widget.get_menu() is not None or self._menu_can_be_none_pack, \
f'{widget.get_class_id()} menu cannot be None'
assert widget.get_id() not in self._widgets.keys(), \
f'{widget.get_class_id()} already exists in {self.get_class_id()}'
assert widget.get_menu() == self._menu or widget.get_menu() is None, \
'widget menu to be added to frame must be in same menu as frame, or ' \
'it can have any Menu instance'
assert widget.get_frame() is None, \
f'{widget.get_class_id()} is already packed in {widget.get_frame().get_class_id()}'
assert_alignment(align)
assert vertical_position in (POSITION_NORTH, POSITION_CENTER, POSITION_SOUTH), \
'vertical position must be NORTH, CENTER, or SOUTH'
assert_vector(margin, 2)
assert widget.configured, \
f'{widget.get_class_id()} must be configured before packing'
if widget.get_margin() != (0, 0) and self._pack_margin_warning:
if self._verbose:
warn(
f'{widget.get_class_id()} margin should be (0, 0) if packed, but'
f' received {widget.get_margin()}; {self.get_class_id()}.pack() '
f'does not consider previous widget margin. Set '
f'frame._pack_margin_warning=False to hide this warning'
)
if isinstance(widget, Frame):
widget.update_indices()
widget.set_frame(self)
widget.set_margin(*margin)
if self._frame_scrollarea is not None:
widget.set_scrollarea(self._frame_scrollarea)
else:
widget.set_scrollarea(self._scrollarea)
if self.is_scrollable or self._has_title or isinstance(widget, Frame):
self._sort_menu_update_frames()
self._widgets[widget.get_id()] = widget
self._widgets_props[widget.get_id()] = (align, vertical_position)
# Sort widgets to keep selection order
menu_widgets = self._get_menu_widgets()
if widget.get_menu() is not None and widget in menu_widgets:
self._menu._validate_frame_widgetmove = False
widgets_list = list(self._widgets.values())
# Move frame to last
if len(self._widgets) > 1:
w_last = widgets_list[-2] # -1 is the last added
for i in range(2, len(self._widgets)):
if w_last.get_menu() is None and len(self._widgets) > 2:
w_last = widgets_list[-(i + 1)]
else:
break
# Check for last if w_last is frame
while True:
if not (isinstance(w_last, Frame) and
w_last.get_indices() != (-1, -1)) or \
w_last.get_menu() is None:
break
w_last = menu_widgets[w_last.last_index]
if w_last.get_menu() == self._menu:
self._menu.move_widget_index(self, w_last, render=False)
# Swap
self._menu.move_widget_index(widget, self, render=False)
if isinstance(widget, Frame):
reverse = menu_widgets.index(widget) == len(menu_widgets) - 1
widgs = widget.get_widgets(unpack_subframes_include_frame=True, reverse=reverse)
first_moved_widget = None
last_moved_widget = None
for w in widgs:
if w.get_menu() is None or w not in menu_widgets:
continue
self._menu.move_widget_index(w, self, render=False)
if first_moved_widget is None:
first_moved_widget = w
last_moved_widget = w
swap_target = last_moved_widget if reverse else first_moved_widget
if swap_target is not None:
menu_widgets.remove(widget)
menu_widgets.insert(menu_widgets.index(swap_target), widget)
# Move widget to first
menu_widgets.remove(self)
for k in range(len(widgets_list)):
if widgets_list[k].get_menu() == self._menu:
menu_widgets.insert(menu_widgets.index(widgets_list[k]), self)
break
self._menu._validate_frame_widgetmove = True
# Update control widget
if self._control_widget is None:
self._control_widget = widget
self._control_widget_last_pos = self._control_widget.get_position()
if isinstance(widget, Frame):
self._has_frames = True
# Update menu selected widget
if self._menu is not None:
self._menu.move_widget_index(None, update_selected_index=True)
# Render is mandatory as it modifies row/column layout
try:
self.update_position()
self._menu_render()
except _FrameSizeException:
self.unpack(widget)
raise
# Request scroll if widget is selected
if widget.is_selected():
widget.scroll_to_widget()
widget.scroll_to_widget()
# Update widget leave
check_widget_mouseleave()
return widget
[docs] def hide(self) -> 'Frame':
super(Frame, self).hide()
if self._has_title:
self._frame_title.hide()
# sub-widgets cannot be hidden because some widgets compute sizing even
# if the frame itself is hidden
# for w in self.get_widgets(unpack_subframes=False):
# w.hide()
return self
[docs] def show(self) -> 'Frame':
super(Frame, self).show()
# same as hiding, sub-widgets should not be modified
# for w in self.get_widgets(unpack_subframes=False):
# w.show()
if self._has_title:
self._frame_title.show()
return self
[docs] def update_indices(self) -> 'Frame':
"""
Update first and last selectable widget index.
:return: Self reference
"""
# Public index update only triggered if frame does not contain subframes
if not self._has_frames:
self._update_indices()
return self
def _update_indices(self) -> None:
"""
Private update indices method.
"""
self.first_index = -1
self.last_index = -1
for widget in self.get_widgets(unpack_subframes=False):
if (widget.is_selectable or isinstance(widget, Frame)) and \
widget.get_menu() is not None:
if isinstance(widget, Frame) and widget.get_indices() == (-1, -1):
continue # Frames with not selectable indices are not counted
windex = widget.get_col_row_index()[2]
if self.first_index == -1:
self.first_index = windex
self.last_index = windex
else:
self.first_index = min(self.first_index, windex)
self.last_index = max(self.last_index, windex)
if self.get_frame() is not None:
self.get_frame()._update_indices()
[docs] def update(self, events: EventVectorType) -> bool:
self.apply_update_callbacks(events)
updated = False
if self.readonly or not self.is_visible():
self._readonly_check_mouseover(events)
return updated
# Check title events
if self._has_title:
# Check for buttons
for w in self._frame_title.get_widgets():
updated = updated or w.update(events)
# Check if clicked the title for drag
for event in events:
# Check mouseover
self._frame_title._check_mouseover(event)
# If clicked in title
if (event.type == pygame.MOUSEBUTTONDOWN and self._mouse_enabled and event.button in
(1, 2, 3) or event.type == FINGERDOWN and self._touchscreen_enabled and self._menu is not None) and \
self._draggable:
event_pos = get_finger_pos(self._menu, event)
if self._frame_title.get_rect(to_real_position=True).collidepoint(*event_pos):
if not self._frame_title.get_attribute('drag', False):
self._frame_title.set_attribute('drag', True)
updated = True
# User releases the button
elif event.type == pygame.MOUSEBUTTONUP and self._mouse_enabled and \
event.button in (1, 2, 3) or \
event.type == FINGERUP and self._touchscreen_enabled:
self._frame_title.set_attribute('drag', False)
# Mouse out from window
# elif event.type == pygame.ACTIVEEVENT:
# if event.gain != 1:
# self._frame_title.set_attribute('drag', False)
# break
# User moves the mouse while drag
elif event.type == pygame.MOUSEMOTION and hasattr(event, 'rel') or \
event.type == FINGERMOTION and self._touchscreen_enabled and self._menu is not None:
if self._frame_title.get_attribute('drag', False) and self._draggable:
# Get relative movement
rx = event.rel[0] if event.type == pygame.MOUSEMOTION else \
event.dx * self._menu.get_window_size()[0] * S_FINGER_FACTOR[0]
ry = event.rel[1] if event.type == pygame.MOUSEMOTION else \
event.dy * self._menu.get_window_size()[1] * S_FINGER_FACTOR[1]
event_pos = get_finger_pos(self._menu, event)
tx, ty = self.get_translate()
title_rect = self._frame_title.get_rect(to_real_position=True)
if self._rect.y <= 0:
if not title_rect.collidepoint(*event_pos):
if ry > 0:
self.translate(tx, ty - self._rect.y)
self.force_menu_surface_update()
updated = True
continue
elif self.get_scrollarea() is not None:
max_v = self.get_scrollarea().get_world_size()[1] - self._title_height()
if self._rect.y >= max_v:
if not title_rect.collidepoint(*event_pos):
if ry < 0:
continue
if ry > 0:
continue
if ry > 0 and self._rect.y + ry >= max_v:
continue
# Get the max/min distance which can translate in vertical
if ry < 0:
ry = -min(-ry, self._rect.y)
self.translate(tx + rx, ty + ry)
self.force_menu_surface_update()
updated = True
# Check mouseover
for event in events:
if self._check_mouseover(event):
break
# If not scrollable, return
if not self.is_scrollable:
return updated
return updated or self._frame_scrollarea.update(events)
class _FrameSizeException(Exception):
"""
If widget size is greater than frame raises exception.
"""
pass
class _FrameDoNotAcceptScrollarea(Exception):
"""
Raised if the frame does not accept a scrollarea.
"""
pass
class _FrameDoNotAcceptTitle(Exception):
"""
Raised if the frame does not accept a title.
"""
pass
class FrameManager(AbstractWidgetManager, ABC):
"""
Frame manager.
"""
def _frame(
self,
width: NumberType,
height: NumberType,
orientation: str,
frame_id: str = '',
**kwargs
) -> 'pygame_menu.widgets.Frame':
"""
Adds a frame to the Menu.
:param width: Frame width in px
:param height: Frame height in px
:param orientation: Frame orientation, horizontal or vertical. See :py:mod:`pygame_menu.locals`
:param frame_id: ID of the frame
:param kwargs: Optional keyword arguments
:return: Widget object
:rtype: :py:class:`pygame_menu.widgets.Frame`
"""
from pygame_menu._scrollarea import get_scrollbars_from_position
# Remove invalid keys from kwargs
for key in list(kwargs.keys()):
if key not in ('align', 'background_color', 'background_inflate',
'border_color', 'border_inflate', 'border_width',
'cursor', 'margin', 'padding', 'max_height', 'max_width',
'scrollbar_color', 'scrollbar_cursor',
'scrollbar_shadow_color', 'scrollbar_shadow_offset',
'scrollbar_shadow_position', 'scrollbar_shadow',
'scrollbar_slider_color', 'scrollbar_slider_pad',
'scrollbar_thick', 'scrollbars', 'scrollarea_color',
'border_position', 'scrollbar_slider_hover_color',
'tab_size', 'float', 'float_origin_position'):
kwargs.pop(key, None)
attributes = self._filter_widget_attributes(kwargs)
pad = parse_padding(attributes['padding']) # top, right, bottom, left
pad_h = pad[1] + pad[3]
pad_v = pad[0] + pad[2]
assert width > pad_h, \
f'frame width ({width}) cannot be lower than horizontal padding size ({pad_h})'
assert height > pad_v, \
f'frame height ({height}) cannot be lower than vertical padding size ({pad_v})'
widget = Frame(
width=width - pad_h,
height=height - pad_v,
orientation=orientation,
frame_id=frame_id
)
self._configure_widget(widget=widget, **attributes)
widget.make_scrollarea(
max_height=kwargs.pop('max_height', height) - pad_v,
max_width=kwargs.pop('max_width', width) - pad_h,
scrollarea_color=kwargs.pop('scrollarea_color', None),
scrollbar_color=kwargs.pop('scrollbar_color', self._theme.scrollbar_color),
scrollbar_cursor=kwargs.pop('scrollbar_cursor', self._theme.scrollbar_cursor),
scrollbar_shadow=kwargs.pop('scrollbar_shadow', self._theme.scrollbar_shadow),
scrollbar_shadow_color=kwargs.pop('scrollbar_shadow_color', self._theme.scrollbar_shadow_color),
scrollbar_shadow_offset=kwargs.pop('scrollbar_shadow_offset', self._theme.scrollbar_shadow_offset),
scrollbar_shadow_position=kwargs.pop('scrollbar_shadow_position', self._theme.scrollbar_shadow_position),
scrollbar_slider_color=kwargs.pop('scrollbar_slider_color', self._theme.scrollbar_slider_color),
scrollbar_slider_hover_color=kwargs.pop('scrollbar_slider_hover_color', self._theme.scrollbar_slider_hover_color),
scrollbar_slider_pad=kwargs.pop('scrollbar_slider_pad', self._theme.scrollbar_slider_pad),
scrollbar_thick=kwargs.pop('scrollbar_thick', self._theme.scrollbar_thick),
scrollbars=get_scrollbars_from_position(kwargs.pop('scrollbars', self._theme.scrollarea_position))
)
self._append_widget(widget)
self._check_kwargs(kwargs)
return widget
def frame_h(
self,
width: NumberType,
height: NumberType,
frame_id: str = '',
**kwargs
) -> 'pygame_menu.widgets.Frame':
"""
Adds a horizontal frame to the Menu. Frame is a widget container that
packs many widgets within. All contained widgets have a floating position,
and use only 1 position in column/row layout.
.. code-block:: python
frame.pack(W1, alignment=ALIGN_LEFT, vertical_position=POSITION_NORTH)
frame.pack(W2, alignment=ALIGN_LEFT, vertical_position=POSITION_CENTER)
frame.pack(W3, alignment=ALIGN_LEFT, vertical_position=POSITION_SOUTH)
...
----------------
|W1 |
| W2 ... |
| W3 |
----------------
kwargs (Optional)
- ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_
- ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color
- ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px
- ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color
- ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px
- ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals`
- ``border_width`` (int) – Border width in px. If ``0`` disables the border
- ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the frame if the mouse is placed over
- ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows
- ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating
- ``margin`` (tuple, list) – Widget (left, bottom) margin in px
- ``max_height`` (int) – Max height in px. If this value is lower than the frame ``height`` a scrollbar will appear on vertical axis. ``None`` by default (same height)
- ``max_width`` (int) – Max width in px. If this value is lower than the frame ``width`` a scrollbar will appear on horizontal axis. ``None`` by default (same width)
- ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left)
- ``scrollarea_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`,None) – Scroll area color. If ``None`` area is transparent
- ``scrollbar_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Scrollbar color
- ``scrollbar_cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the scrollbars if the mouse is placed over
- ``scrollbar_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the shadow of each scrollbar
- ``scrollbar_shadow_offset`` (int) – Offset of the scrollbar shadow in px
- ``scrollbar_shadow_position`` (str) – Position of the scrollbar shadow. See :py:mod:`pygame_menu.locals`
- ``scrollbar_shadow`` (bool) – Indicate if a shadow is drawn on each scrollbar
- ``scrollbar_slider_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the sliders
- ``scrollbar_slider_hover_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the slider if hovered or clicked
- ``scrollbar_slider_pad`` (int, float) – Space between slider and scrollbars borders in px
- ``scrollbar_thick`` (int) – Scrollbar thickness in px
- ``scrollbars`` (str) – Scrollbar position. See :py:mod:`pygame_menu.locals`
- ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow
- ``shadow_radius`` (int) - Border radius of the shadow
- ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'``
- ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled
.. note::
All theme-related optional kwargs use the default Menu theme if not
defined.
.. note::
If horizontal frame contains a scrollarea (setting ``max_height`` or
``max_width`` less than size) padding will be set at zero.
.. note::
Packing applies a virtual translation to the widget, previous translation
is not modified.
.. note::
Widget floating is also considered within frames. If a widget is
floating, it does not add any size to the respective positioning.
.. note::
The Frame size created with this method does consider the padding. Thus,
if Frame is created with ``width=100``, ``height=200`` and ``padding=25``
the final internal size is ``width=50`` and ``height=150``.
.. note::
This is applied only to the base Menu (not the currently displayed,
stored in ``_current`` pointer); for such behaviour apply to
:py:meth:`pygame_menu.menu.Menu.get_current` object.
:param width: Frame width in px
:param height: Frame height in px
:param frame_id: ID of the horizontal frame
:param kwargs: Optional keyword arguments
:return: Widget object
:rtype: :py:class:`pygame_menu.widgets.Frame`
"""
return self._frame(width, height, ORIENTATION_HORIZONTAL, frame_id, **kwargs)
def frame_v(
self,
width: NumberType,
height: NumberType,
frame_id: str = '',
**kwargs
) -> 'pygame_menu.widgets.Frame':
"""
Adds a vertical frame to the Menu. Frame is a widget container that packs
many widgets within. All contained widgets have a floating position, and
use only 1 position in column/row layout.
.. code-block:: python
frame.pack(W1, alignment=ALIGN_LEFT)
frame.pack(W2, alignment=ALIGN_CENTER)
frame.pack(W3, alignment=ALIGN_RIGHT)
...
--------
|W1 |
| W2 |
| W3|
| ... |
--------
kwargs (Optional)
- ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_
- ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color
- ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px
- ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color
- ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px
- ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals`
- ``border_width`` (int) – Border width in px. If ``0`` disables the border
- ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the frame if the mouse is placed over
- ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows
- ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating
- ``margin`` (tuple, list) – Widget (left, bottom) margin in px
- ``max_height`` (int) – Max height in px. If this value is lower than the frame ``height`` a scrollbar will appear on vertical axis. ``None`` by default (same height)
- ``max_width`` (int) – Max width in px. If this value is lower than the frame ``width`` a scrollbar will appear on horizontal axis. ``None`` by default (same width)
- ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left)
- ``scrollarea_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`,None) – Scroll area color. If ``None`` area is transparent
- ``scrollbar_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Scrollbar color
- ``scrollbar_cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the scrollbars if the mouse is placed over
- ``scrollbar_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the shadow of each scrollbar
- ``scrollbar_shadow_offset`` (int) – Offset of the scrollbar shadow in px
- ``scrollbar_shadow_position`` (str) – Position of the scrollbar shadow. See :py:mod:`pygame_menu.locals`
- ``scrollbar_shadow`` (bool) – Indicate if a shadow is drawn on each scrollbar
- ``scrollbar_slider_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the sliders
- ``scrollbar_slider_hover_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the slider if hovered or clicked
- ``scrollbar_slider_pad`` (int, float) – Space between slider and scrollbars borders in px
- ``scrollbar_thick`` (int) – Scrollbar thickness in px
- ``scrollbars`` (str) – Scrollbar position. See :py:mod:`pygame_menu.locals`
- ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow
- ``shadow_radius`` (int) - Border radius of the shadow
- ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'``
- ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled
.. note::
All theme-related optional kwargs use the default Menu theme if not
defined.
.. note::
If vertical frame contains a scrollarea (setting ``max_height`` or
``max_width`` less than size) padding will be set at zero.
.. note::
Packing applies a virtual translation to the widget, previous translation
is not modified.
.. note::
Widget floating is also considered within frames. If a widget is
floating, it does not add any size to the respective positioning.
.. note::
The Frame size created with this method does consider the padding. Thus,
if Frame is created with ``width=100``, ``height=200`` and ``padding=25``
the final internal size is ``width=50`` and ``height=150``.
.. note::
This is applied only to the base Menu (not the currently displayed,
stored in ``_current`` pointer); for such behaviour apply to
:py:meth:`pygame_menu.menu.Menu.get_current` object.
:param width: Frame width in px
:param height: Frame height in px
:param frame_id: ID of the vertical frame
:param kwargs: Optional keyword arguments
:return: Widget object
:rtype: :py:class:`pygame_menu.widgets.Frame`
"""
return self._frame(width, height, ORIENTATION_VERTICAL, frame_id, **kwargs)