diff --git a/arcade/gui/widgets/layout.py b/arcade/gui/widgets/layout.py index cf98030d7..4d7fa3e2b 100644 --- a/arcade/gui/widgets/layout.py +++ b/arcade/gui/widgets/layout.py @@ -9,19 +9,36 @@ class UIAnchorLayout(UILayout): """ Places children based on anchor values. - Defaults to `size_hint = (1, 1)`. - Supports `size_hint`, `size_hint_min`, and `size_hint_max`. - Children may overlap. + Defaults to ``size_hint = (1, 1)``. - Child are resized based on size_hint. Max and Min size_hints only take effect if a size_hint is given. + Supports the options ``size_hint``, ``size_hint_min``, and + ``size_hint_max``. Children are allowed to overlap. - Allowed keyword options for `UIAnchorLayout.add()` - - anchor_x: str = None - uses `self.default_anchor_x` as default - - align_x: float = 0 - - anchor_y: str = None - uses `self.default_anchor_y` as default - - align_y: float = 0 + Child are resized based on ``size_hint``. Maximum and minimum + ``size_hint``s only take effect if a ``size_hint`` is given. + Allowed keyword options for + :py:meth:`~arcade.gui.UIAnchorLayout.add`:: + - ``anchor_x``: ``str`` = ``None`` + + Horizontal anchor position for the layout. The class constant + :py:attr:`~arcade.gui.UIAnchorLayout.default_anchor_x` is used as + default. + + - ``anchor_y``: ``str`` = ``None`` + + Vertical anchor position for the layout. The class constant + :py:attr:`~arcade.gui.UIAnchorLayout.default_anchor_y` is used as + default. + + - ``align_x``: ``float`` = 0 + + Horizontal alignment for the layout. + + - ``align_y``: ``float`` = 0 + + Vertical alignement for the layout. """ default_anchor_x = "center" @@ -66,20 +83,22 @@ def add( **kwargs ) -> W: """ - Add a widget to this :class:`UIWidget` as a child. - Added widgets will receive ui events and be rendered. - - By default, the latest added widget will receive ui events first and will be rendered on top of others. - - The widgets will be automatically placed within this widget. - - :param child: widget to add - :param anchor_x: anchor for x-axis, can be left, center, right - :param align_x: offset for the given anchor - :param anchor_y: anchor for y-axis, can be top, center, bottom - :param align_y: offset for the given anchor - - :return: given child + Add a widget to the layout as a child. Added widgets will receive + all user-interface events and be rendered. + + By default, the latest added widget will receive events first and will + be rendered on top of others. The widgets will be automatically placed + within this widget. + + :param child: Specified child widget to add. + :param anchor_x: Horizontal anchor. Valid options are ``left``, + ``right``, and ``center``. + :param align_x: Offset or padding for the horizontal anchor. + :param anchor_y: Vertical anchor. Valid options are ``top``, + ``center``, and ``bottom``. + :param align_y: Offset or padding for the vertical anchor. + + :return: Given child that was just added to the layout. """ return super(UIAnchorLayout, self).add( child=child, @@ -124,10 +143,10 @@ def _place_child( if shmx_h: new_child_rect = new_child_rect.max_size(height=shmx_h) - # stay in bounds + # Stay in bounds new_child_rect = new_child_rect.max_size(*self.content_size) - # calculate position + # Calculate position content_rect = self.content_rect anchor_x = "center_x" if anchor_x == "center" else anchor_x @@ -140,37 +159,49 @@ def _place_child( own_anchor_y_value = getattr(content_rect, anchor_y) diff_y = own_anchor_y_value + align_y - child_anchor_y_value - # check if changes are required + # Check if changes are required if diff_x or diff_y or child.rect != new_child_rect: child.rect = new_child_rect.move(diff_x, diff_y) class UIBoxLayout(UILayout): """ - Places widgets next to each other. - Depending on the vertical attribute, the widgets are placed top to bottom or left to right. - - Hint: UIBoxLayout does not adjust its own size if children are added. - This requires a UIManager or UIAnchorLayout as parent. - Use `self.fit_content()` to resize, bottom-left is used as anchor point. - - UIBoxLayout supports: size_hint, size_hint_min, size_hint_max - - If a child widget provides a size_hint for a dimension, the child will be resized within the given range of - size_hint_min and size_hint_max (unrestricted if not given). - For vertical=True any available space (layout size - min_size of children) will be distributed to the child widgets - based on their size_hint. - - :param float x: x coordinate of bottom left - :param float y: y coordinate of bottom left - :param vertical: Layout children vertical (True) or horizontal (False) - :param align: Align children in orthogonal direction (x: left, center, right / y: top, center, bottom) - :param children: Initial children, more can be added - :param size_hint: A hint for :class:`UILayout`, if this :class:`UIWidget` - would like to grow (default 0,0 -> minimal size to contain children) - :param size_hint_min: min width and height in pixel - :param size_hint_max: max width and height in pixel - :param space_between: Space between the children + Place widgets next to each other. Depending on the + :py:class:`~arcade.gui.UIBoxLayout.vertical` attribute, the widgets are + placed top to bottom or left to right. + + .. hint:: + + :py:class:`~arcade.gui.UIBoxLayout` does not adjust its + own size if children are added. This requires a + :py:class:`~arcade.gui.UIManager` or a + :py:class:`~arcade.gui.UIAnchorLayout` as a parent. + + Use :py:meth:`arcade.gui.UIBoxLayout.fit_content` to resize the layout. The + bottom-left corner is used as the default anchor point. + + Supports the options: ``size_hint``, ``size_hint_min``, ``size_hint_max``. + + If a child widget provides a ``size_hint`` for a dimension, the child will + be resized within the given range of ``size_hint_min`` and + ``size_hint_max`` (unrestricted if not given). If the parameter + ``vertical`` is True, any available space (``layout size - min_size`` of + children) will be distributed to the child widgets based on their + ``size_hint``. + + :param float x: ``x`` coordinate of the bottom left corner. + :param float y: ``y`` coordinate of the bottom left corner. + :param vertical: Layout children vertical (True) or horizontal (False). + :param align: Align children in orthogonal direction:: + - ``x``: ``left``, ``center``, and ``right`` + - ``y``: ``top``, ``center``, and ``bottom`` + :param children: Initial list of children. More can be added later. + :param size_hint: Size hint for the :py:class:`~arcade.gui.UILayout` if + the widget would like to grow. Defaults to ``0, 0`` -> + minimal size to contain children. + :param size_hint_min: Minimum width and height in pixels. + :param size_hint_max: Maximum width and height in pixels. + :param space_between: Space in pixels between the children. """ def __init__( @@ -207,32 +238,39 @@ def __init__( bind(self, "_children", self._update_size_hints) - # initially update size hints + # Initially update size hints self._update_size_hints() @staticmethod def _layouting_allowed(child: UIWidget) -> Tuple[bool, bool]: """ - Checks if size_hint is given for the dimension, which would allow the layout to resize this widget + Checks if ``size_hint`` is given for the dimension. This would allow + the layout to resize this widget. - :return: horizontal, vertical + :return: Horizontal and vertical. """ sh_w, sh_h = child.size_hint or (None, None) + return sh_w is not None, sh_h is not None def _update_size_hints(self): - required_space_between = max(0, len(self.children) - 1) * self._space_between + required_space_between = max(0, len(self.children) - 1) * \ + self._space_between def min_size(child: UIWidget) -> Tuple[float, float]: """ - Determine min size of a child widget - This can be the size_hint_min. If no size_hints are provided the child size has to stay the same and - the minimal size is the current size. + Determine the minimum size of a child widget. + + This can be the minimum size hint (``size_hint_min``). If no size + hints are provided the child size has to stay the same and the + minimal size is the current size. """ h_allowed, v_allowed = UIBoxLayout._layouting_allowed(child) + shmn_w, shmn_h = child.size_hint_min or (None, None) shmn_w = shmn_w or 0 if h_allowed else child.width shmn_h = shmn_h or 0 if v_allowed else child.height + return shmn_w, shmn_h min_child_sizes = [min_size(child) for child in self.children] @@ -240,22 +278,28 @@ def min_size(child: UIWidget) -> Tuple[float, float]: if len(self.children) == 0: width = 0 height = 0 + elif self.vertical: width = max(size[0] for size in min_child_sizes) height_of_children = sum(size[1] for size in min_child_sizes) height = height_of_children + required_space_between + else: width_of_children = sum(size[0] for size in min_child_sizes) width = width_of_children + required_space_between height = max(size[1] for size in min_child_sizes) - base_width = self._padding_left + self._padding_right + 2 * self._border_width - base_height = self._padding_top + self._padding_bottom + 2 * self._border_width + base_width = self._padding_left + self._padding_right + 2 \ + * self._border_width + base_height = self._padding_top + self._padding_bottom + 2 \ + * self._border_width + self.size_hint_min = base_width + width, base_height + height def fit_content(self): """ - Resize to fit content, using `self.size_hint_min` + Resize to fit content, using + the :py:attr:`~arcade.gui.UIBoxLayout.size_hint_min` attribute. :return: self """ @@ -271,7 +315,8 @@ def do_layout(self): if self.vertical: available_width = self.content_width - # calculate if some space is available for children to grow + + # Determine if some space is available for children to grow available_height = max(0, self.height - self.size_hint_min[1]) total_size_hint_height = ( sum( @@ -280,24 +325,23 @@ def do_layout(self): if child.size_hint ) or 1 - ) # prevent division by zero + ) # Prevent division by zero for child in self.children: new_rect = child.rect - # collect all size hints + # Collect all size hints sh_w, sh_h = child.size_hint or (None, None) shmn_w, shmn_h = child.size_hint_min or (None, None) shmx_w, shmx_h = child.size_hint_max or (None, None) - # apply y-axis + # Apply y-axis if sh_h is not None: min_height_value = shmn_h or 0 # Maximal growth to parent.width * shw - available_growth_height = min_height_value + available_height * ( - sh_h / total_size_hint_height - ) + available_growth_height = min_height_value + \ + available_height * (sh_h / total_size_hint_height) max_growth_height = self.height * sh_h new_rect = new_rect.resize( height=min(available_growth_height, max_growth_height) @@ -305,10 +349,11 @@ def do_layout(self): if shmn_h is not None: new_rect = new_rect.min_size(height=shmn_h) + if shmx_h is not None: new_rect = new_rect.max_size(height=shmx_h) - # apply x-axis + # Apply x-axis if sh_w is not None: new_rect = new_rect.resize( width=max(available_width * sh_w, shmn_w or 0) @@ -319,11 +364,12 @@ def do_layout(self): if shmx_w is not None: new_rect = new_rect.max_size(width=shmx_w) - # align + # Align the children if self.align == "left": new_rect = new_rect.align_left(start_x) elif self.align == "right": - new_rect = new_rect.align_right(start_x + self.content_width) + new_rect = new_rect.align_right( + start_x + self.content_width) else: center_x = start_x + self.content_width // 2 new_rect = new_rect.align_center_x(center_x) @@ -337,7 +383,8 @@ def do_layout(self): center_y = start_y - self.content_height // 2 available_height = self.content_height - # calculate if some space is available for children to grow + + # Calculate if some space is available for children to grow. available_width = max(0, self.width - self.size_hint_min[0]) total_size_hint_width = ( sum( @@ -346,37 +393,41 @@ def do_layout(self): if child.size_hint ) or 1 - ) # prevent division by zero + ) # Prevent division by zero - # TODO Fix layout algorithm, handle size hints per dimension! - # 0. check if any hint given, if not, continue with step 4. - # 1. change size to minimal - # 2. grow using size_hint - # 3. ensure size_hint_max - # 4. place child + # TODO + # Fix layout algorithm and handle size hints per dimension! + # + # 0. Check if any hints are given. If not, continue with step 4. + # 1. Change size to minimal. + # 2. Grow using size_hint. + # 3. Ensure size_hint_max. + # 4. Place child for child in self.children: new_rect = child.rect - # collect all size hints + # Collect all size hints sh_w, sh_h = child.size_hint or (None, None) + shmn_w, shmn_h = child.size_hint_min or (None, None) shmx_w, shmx_h = child.size_hint_max or (None, None) - # apply x-axis + # Apply x-axis if sh_w is not None: min_width_value = shmn_w or 0 - # new_rect = new_rect.resize(width=min_width_value) # TODO should not be required! + + # TODO: this should not be required + # new_rect = new_rect.resize(width=min_width_value) # Maximal growth to parent.width * shw - available_growth_width = min_width_value + available_width * ( - sh_w / total_size_hint_width - ) + available_growth_width = min_width_value + available_width \ + * (sh_w / total_size_hint_width) max_growth_width = self.width * sh_w new_rect = new_rect.resize( width=min( available_growth_width, max_growth_width - ) # this does not enforce min width + ) # This does not enforce the minimum width ) if shmn_w is not None: @@ -385,7 +436,7 @@ def do_layout(self): if shmx_w is not None: new_rect = new_rect.max_size(width=shmx_w) - # apply y-axis + # Apply vertical axis if sh_h is not None: new_rect = new_rect.resize( height=max(available_height * sh_h, shmn_h or 0) @@ -397,11 +448,12 @@ def do_layout(self): if shmx_h is not None: new_rect = new_rect.max_size(height=shmx_h) - # align + # Align all children if self.align == "top": new_rect = new_rect.align_top(start_y) elif self.align == "bottom": - new_rect = new_rect.align_bottom(start_y - self.content_height) + new_rect = new_rect.align_bottom( + start_y - self.content_height) else: new_rect = new_rect.align_center_y(center_y) @@ -414,19 +466,28 @@ def do_layout(self): class UIGridLayout(UILayout): """ - Places widget in a grid layout. - :param float x: x coordinate of bottom left - :param float y: y coordinate of bottom left - :param str align_horizontal: Align children in orthogonal direction (x: left, center, right) - :param str align_vertical: Align children in orthogonal direction (y: top, center, bottom) - :param Iterable[UIWidget] children: Initial children, more can be added - :param size_hint: A hint for :class:`UILayout`, if this :class:`UIWidget` would like to grow - :param size_hint_min: Min width and height in pixel - :param size_hint_max: Max width and height in pixel - :param horizontal_spacing: Space between columns - :param vertical_spacing: Space between rows - :param int column_count: Number of columns in the grid, can be changed - :param int row_count: Number of rows in the grid, can be changed + Place widgets in a grid layout. This is similar to tkinter's ``grid`` + layout geometry manager. + + :param float x: ``x`` coordinate of bottom left corner. + :param float y: ``y`` coordinate of bottom left corner. + :param str align_horizontal: Align children in orthogonal direction. + Options include ``left``, ``center``, and + ``right``. + :param str align_vertical: Align children in orthogonal direction. Options + include ``top``, ``center``, and ``bottom``. + :param Iterable[UIWidget] children: Initial list of children. More can be + added later. + :param size_hint: A size hint for :py:class:`~arcade.gui.UILayout`, if the + :py:class:`~arcade.gui.UIWidget` would like to grow. + :param size_hint_min: Minimum width and height in pixels. + :param size_hint_max: Maximum width and height in pixels. + :param horizontal_spacing: Space between columns. + :param vertical_spacing: Space between rows. + :param int column_count: Number of columns in the grid. This can be changed + later. + :param int row_count: Number of rows in the grid. This can be changed + later. """ def __init__( @@ -471,11 +532,10 @@ def __init__( bind(self, "_children", self._update_size_hints) - # initially update size hints + # Initially update size hints self._update_size_hints() def _update_size_hints(self): - child_sorted_row_wise = [ [None for _ in range(self.column_count)] for _ in range(self.row_count) ] @@ -521,18 +581,22 @@ def _update_size_hints(self): max(width / (span or 1) for width, span in col) ) - base_width = self._padding_left + self._padding_right + 2 * self._border_width - base_height = self._padding_top + self._padding_bottom + 2 * self._border_width + base_width = self._padding_left + self._padding_right + \ + 2 * self._border_width + base_height = self._padding_top + self._padding_bottom + \ + 2 * self._border_width content_height = ( - sum(principal_height_ratio_list) + self.row_count * self._vertical_spacing + sum(principal_height_ratio_list) + self.row_count * \ + self._vertical_spacing ) content_width = ( sum(principal_width_ratio_list) + self.column_count * self._horizontal_spacing ) - self.size_hint_min = (base_width + content_width, base_height + content_height) + self.size_hint_min = (base_width + content_width, + base_height + content_height) def add( self, @@ -544,11 +608,14 @@ def add( **kwargs ) -> W: """ - Adds widgets in the grid. - - :param UIWidget child: The widget which is to be added in the grid - :param int col_num: The column number in which the widget is to be added (first column is numbered 0; left) - :param int row_num: The row number in which the widget is to be added (first row is numbered 0; top) + Add a widget to the grid layout. + + :param UIWidget child: Specified child widget to add. + :param int col_num: Column index in which the widget is to be added. + The first column is numbered 0; which is the top + left corner. + :param int row_num: The row number in which the widget is to be added. + The first row is numbered 0; which is the :param int col_span: Number of columns the widget will stretch for. :param int row_span: Number of rows the widget will stretch for. """ @@ -600,7 +667,7 @@ def do_layout(self): ]: row[col_num : col_num + col_span] = [child] * col_span # noqa: E203 - # making max_height_per_row and max_width_per_column uniform + # Making max_height_per_row and max_width_per_column uniform for row in max_height_per_row: principal_height_ratio = max(height / (span or 1) for height, span in row) for i, (height, span) in enumerate(row): @@ -613,17 +680,19 @@ def do_layout(self): if width / (span or 1) < principal_width_ratio: col[i] = (principal_width_ratio * span, span) - # row wise rendering children + # Row wise rendering children for row_num, row in enumerate(child_sorted_row_wise): max_height_row = 0 start_x = initial_left_x for col_num, child in enumerate(row): max_height = ( - max_height_per_row[row_num][col_num][0] + self._vertical_spacing + max_height_per_row[row_num][col_num][0] + \ + self._vertical_spacing ) max_width = ( - max_width_per_column[col_num][row_num][0] + self._horizontal_spacing + max_width_per_column[col_num][row_num][0] + \ + self._horizontal_spacing ) if max_width == self._horizontal_spacing: