From 2fa1be3729aa1eaf8004e111b75cbe6c7a8008b3 Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Sun, 16 Apr 2023 19:05:16 +0100 Subject: [PATCH 1/2] Dynamic sizing fixes part 1 --- .../core/interfaces/element_interface.py | 5 ++- pygame_gui/core/ui_container.py | 6 +++- pygame_gui/elements/ui_button.py | 29 ++++++++-------- pygame_gui/elements/ui_label.py | 32 ++++++++--------- pygame_gui/elements/ui_text_box.py | 13 ++++--- pygame_gui/elements/ui_window.py | 34 +++---------------- 6 files changed, 47 insertions(+), 72 deletions(-) diff --git a/pygame_gui/core/interfaces/element_interface.py b/pygame_gui/core/interfaces/element_interface.py index 1be6e8fb..75477035 100644 --- a/pygame_gui/core/interfaces/element_interface.py +++ b/pygame_gui/core/interfaces/element_interface.py @@ -85,7 +85,8 @@ def set_position(self, position: Union[pygame.math.Vector2, @abstractmethod def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], - Tuple[float, float]]): + Tuple[float, float]], + clamp_to_container: bool = False): """ Method to directly set the dimensions of an element. @@ -93,6 +94,8 @@ def set_dimensions(self, dimensions: Union[pygame.math.Vector2, may make a mess of them. :param dimensions: The new dimensions to set. + :param clamp_to_container: Whether we should clamp the dimensions to the + dimensions of the container or not. """ diff --git a/pygame_gui/core/ui_container.py b/pygame_gui/core/ui_container.py index 18fb9e03..c43a5492 100644 --- a/pygame_gui/core/ui_container.py +++ b/pygame_gui/core/ui_container.py @@ -198,12 +198,16 @@ def set_relative_position(self, position: Union[pygame.math.Vector2, def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], - Tuple[float, float]]): + Tuple[float, float]], + clamp_to_container: bool = False + ): """ Set the dimension of this container and update the positions of elements within it accordingly. :param dimensions: the new dimensions. + :param clamp_to_container: Whether we should clamp the dimensions to the + dimensions of the container or not. """ super().set_dimensions(dimensions) diff --git a/pygame_gui/elements/ui_button.py b/pygame_gui/elements/ui_button.py index 494a4a3f..f7901877 100644 --- a/pygame_gui/elements/ui_button.py +++ b/pygame_gui/elements/ui_button.py @@ -83,8 +83,6 @@ def __init__(self, relative_rect: Union[pygame.Rect, Tuple[float, float], pygame if text_kwargs is not None: self.text_kwargs = text_kwargs - self.dynamic_width = False - self.dynamic_height = False self.dynamic_dimensions_orig_top_left = rel_rect.topleft # support for an optional 'tool tip' element attached to this button self.tool_tip_text = tool_tip_text @@ -502,7 +500,7 @@ def set_text(self, text: str, *, text_kwargs: Optional[Dict[str, str]] = None): any_changed = True if any_changed: - if self.dynamic_width: + if self.dynamic_width or self.dynamic_height: self.rebuild() else: self.drawable_shape.set_text(translate(self.text, **self.text_kwargs)) @@ -708,11 +706,6 @@ def rebuild(self): A complete rebuild of the drawable shape used by this button. """ - self.rect.width = -1 if self.dynamic_width else self.rect.width - self.relative_rect.width = -1 if self.dynamic_width else self.relative_rect.width - - self.rect.height = -1 if self.dynamic_height else self.rect.height - self.relative_rect.height = -1 if self.dynamic_height else self.relative_rect.height theming_parameters = {'normal_bg': self.colours['normal_bg'], 'normal_text': self.colours['normal_text'], @@ -756,25 +749,31 @@ def rebuild(self): 'shape_corner_radius': self.shape_corner_radius, 'transitions': self.state_transitions} + drawable_shape_rect = self.rect.copy() + if self.dynamic_width: + drawable_shape_rect.width = -1 + if self.dynamic_height: + drawable_shape_rect.height = -1 + if self.shape == 'rectangle': - self.drawable_shape = RectDrawableShape(self.rect, theming_parameters, + self.drawable_shape = RectDrawableShape(drawable_shape_rect, theming_parameters, ['normal', 'hovered', 'disabled', 'selected', 'active'], self.ui_manager) elif self.shape == 'ellipse': - self.drawable_shape = EllipseDrawableShape(self.rect, theming_parameters, + self.drawable_shape = EllipseDrawableShape(drawable_shape_rect, theming_parameters, ['normal', 'hovered', 'disabled', 'selected', 'active'], self.ui_manager) elif self.shape == 'rounded_rectangle': - self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters, + self.drawable_shape = RoundedRectangleShape(drawable_shape_rect, theming_parameters, ['normal', 'hovered', 'disabled', 'selected', 'active'], self.ui_manager) self.on_fresh_drawable_shape_ready() - if self.relative_rect.width == -1 or self.relative_rect.height == -1: - self.dynamic_width = self.relative_rect.width == -1 - self.dynamic_height = self.relative_rect.height == -1 + self._on_contents_changed() + def _calc_dynamic_size(self): + if self.dynamic_width or self.dynamic_height: self.set_dimensions(self.image.get_size()) # if we have anchored the left side of our button to the right of it's container then @@ -806,7 +805,7 @@ def on_locale_changed(self): self.font = font self.rebuild() else: - if self.dynamic_width: + if self.dynamic_width or self.dynamic_height: self.rebuild() else: self.drawable_shape.set_text(translate(self.text, **self.text_kwargs)) diff --git a/pygame_gui/elements/ui_label.py b/pygame_gui/elements/ui_label.py index 9a95e0ff..058a3c29 100644 --- a/pygame_gui/elements/ui_label.py +++ b/pygame_gui/elements/ui_label.py @@ -60,8 +60,6 @@ def __init__(self, relative_rect: pygame.Rect, object_id=object_id, element_id='label') - self.dynamic_width = False - self.dynamic_height = False self.dynamic_dimensions_orig_top_left = relative_rect.topleft self.text = text @@ -97,7 +95,6 @@ def set_text(self, text: str, *, text_kwargs: Optional[Dict[str, str]] = None): :param text_kwargs: a dictionary of variable arguments to pass to the translated string useful when you have multiple translations that need variables inserted in the middle. - """ any_changed = False if text != self.text: @@ -112,7 +109,7 @@ def set_text(self, text: str, *, text_kwargs: Optional[Dict[str, str]] = None): any_changed = True if any_changed: - if self.dynamic_width: + if self.dynamic_width or self.dynamic_height: self.rebuild() else: self.drawable_shape.set_text(translate(self.text, **self.text_kwargs)) @@ -123,15 +120,9 @@ def rebuild(self): the displayed text is or remake it with different theming (if the theming has changed). """ - self.rect.width = -1 if self.dynamic_width else self.rect.width - self.relative_rect.width = -1 if self.dynamic_width else self.relative_rect.width - - self.rect.height = -1 if self.dynamic_height else self.rect.height - self.relative_rect.height = -1 if self.dynamic_height else self.relative_rect.height - text_size = self.font.get_rect(translate(self.text, **self.text_kwargs)).size - if ((self.rect.height != -1 and text_size[1] > self.relative_rect.height) or - (self.rect.width != -1 and text_size[0] > self.relative_rect.width)): + if (not (self.dynamic_height or self.dynamic_width) and + ((text_size[1] > self.relative_rect.height) or (text_size[0] > self.relative_rect.width))): width_overlap = self.relative_rect.width - text_size[0] height_overlap = self.relative_rect.height - text_size[1] warn_text = ('Label Rect is too small for text: ' @@ -161,17 +152,22 @@ def rebuild(self): 'text_horiz_alignment_padding': self.text_horiz_alignment_padding, 'text_vert_alignment_padding': self.text_vert_alignment_padding} - self.drawable_shape = RectDrawableShape(self.rect, theming_parameters, + drawable_shape_rect = self.rect.copy() + if self.dynamic_width: + drawable_shape_rect.width = -1 + if self.dynamic_height: + drawable_shape_rect.height = -1 + self.drawable_shape = RectDrawableShape(drawable_shape_rect, theming_parameters, ['normal', 'disabled'], self.ui_manager) self.on_fresh_drawable_shape_ready() - if self.relative_rect.width == -1 or self.relative_rect.height == -1: - self.dynamic_width = self.relative_rect.width == -1 - self.dynamic_height = self.relative_rect.height == -1 + self._on_contents_changed() + def _calc_dynamic_size(self): + if self.dynamic_width or self.dynamic_height: self.set_dimensions(self.image.get_size()) - # if we have anchored the left side of our button to the right of it's container then + # if we have anchored the left side of our button to the right of its container then # changing the width is going to mess up the horiz position as well. new_left = self.relative_rect.left new_top = self.relative_rect.top @@ -290,7 +286,7 @@ def on_locale_changed(self): self.font = font self.rebuild() else: - if self.dynamic_width: + if self.dynamic_width or self.dynamic_height: self.rebuild() else: self.drawable_shape.set_text(translate(self.text, **self.text_kwargs)) diff --git a/pygame_gui/elements/ui_text_box.py b/pygame_gui/elements/ui_text_box.py index afcdf432..04c4c1e4 100644 --- a/pygame_gui/elements/ui_text_box.py +++ b/pygame_gui/elements/ui_text_box.py @@ -98,7 +98,7 @@ def __init__(self, allow_split_dashes: bool = True, plain_text_display_only: bool = False, should_html_unescape_input_text: bool = False): - + relative_rect.height = -1 if wrap_to_height else relative_rect.height super().__init__(relative_rect, manager, container, starting_height=starting_height, layer_thickness=2, @@ -125,7 +125,6 @@ def __init__(self, self._pre_parsing_enabled = pre_parsing_enabled - self.wrap_to_height = wrap_to_height self.link_hover_chunks = [] # container for any link chunks we have self.active_text_effect = None # type: Optional[TextEffect] @@ -213,9 +212,9 @@ def rebuild(self): (self.border_width * 2) - (self.shadow_width * 2) - (2 * self.rounded_corner_offset)))) - if self.wrap_to_height or self.rect[3] == -1: + if self.dynamic_height: self.text_wrap_rect.height = -1 - if self.rect[2] == -1: + if self.dynamic_width: self.text_wrap_rect.width = -1 drawable_area_size = (self.text_wrap_rect[2], self.text_wrap_rect[3]) @@ -223,7 +222,7 @@ def rebuild(self): # This gives us the height of the text at the 'width' of the text_wrap_area self.parse_html_into_style_data() if self.text_box_layout is not None: - if self.wrap_to_height or self.rect[3] == -1 or self.rect[2] == -1: + if self.dynamic_height or self.dynamic_width: final_text_area_size = self.text_box_layout.layout_rect.size new_dimensions = ((final_text_area_size[0] + (self.padding[0] * 2) + (self.border_width * 2) + (self.shadow_width * 2) + @@ -573,7 +572,7 @@ def parse_html_into_style_data(self): default_font_data=default_font_data, allow_split_dashes=self.allow_split_dashes) self.parser.empty_layout_queue() - if self.text_wrap_rect[3] == -1: + if not self.dynamic_height: self.text_box_layout.view_rect.height = self.text_box_layout.layout_rect.height self._align_all_text_rows() @@ -587,7 +586,7 @@ def redraw_from_text_block(self): """ if self.rect.width <= 0 or self.rect.height <= 0: return - if (self.scroll_bar is None and (self.text_wrap_rect[3] != -1) and + if (self.scroll_bar is None and not self.dynamic_height and (self.text_box_layout.layout_rect.height > self.text_wrap_rect[3])): self.rebuild() else: diff --git a/pygame_gui/elements/ui_window.py b/pygame_gui/elements/ui_window.py index 12755d2f..0790bfb0 100644 --- a/pygame_gui/elements/ui_window.py +++ b/pygame_gui/elements/ui_window.py @@ -47,7 +47,7 @@ def __init__(self, self._window_root_container = None # type: Optional[UIContainer] self.resizable = resizable self.draggable = draggable - self.minimum_dimensions = (100, 100) + self.edge_hovering = [False, False, False, False] super().__init__(rect, manager, container=None, @@ -55,6 +55,8 @@ def __init__(self, layer_thickness=1, visible=visible) + self.minimum_dimensions = (100, 100) + if element_id is None: element_id = 'window' @@ -103,26 +105,6 @@ def set_blocking(self, state: bool): """ self.is_blocking = state - def set_minimum_dimensions(self, dimensions: Union[pygame.math.Vector2, - Tuple[int, int], - Tuple[float, float]]): - """ - If this window is resizable, then the dimensions we set here will be the minimum that - users can change the window to. They are also used as the minimum size when - 'set_dimensions' is called. - - :param dimensions: The new minimum dimension for the window. - - """ - self.minimum_dimensions = (min(self.ui_container.rect.width, int(dimensions[0])), - min(self.ui_container.rect.height, int(dimensions[1]))) - - if ((self.rect.width < self.minimum_dimensions[0]) or - (self.rect.height < self.minimum_dimensions[1])): - new_width = max(self.minimum_dimensions[0], self.rect.width) - new_height = max(self.minimum_dimensions[1], self.rect.height) - self.set_dimensions((new_width, new_height)) - def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], Tuple[float, float]]): @@ -133,17 +115,9 @@ def set_dimensions(self, dimensions: Union[pygame.math.Vector2, :param dimensions: The new dimensions to set. """ - # clamp to minimum dimensions and container size - dimensions = (min(self.ui_container.rect.width, - max(self.minimum_dimensions[0], - int(dimensions[0]))), - min(self.ui_container.rect.height, - max(self.minimum_dimensions[1], - int(dimensions[1])))) - # Don't use a basic gate on this set dimensions method because the container may be a # different size to the window - super().set_dimensions(dimensions) + super().set_dimensions(dimensions, clamp_to_container=True) if self._window_root_container is not None: new_container_dimensions = (self.relative_rect.width - (2 * self.shadow_width), From d2582c4d49d268081a0ae42be02fa02a70422799 Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Sun, 16 Apr 2023 19:09:45 +0100 Subject: [PATCH 2/2] add UIElement fixes to dynamic sizing --- pygame_gui/core/ui_element.py | 57 +++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/pygame_gui/core/ui_element.py b/pygame_gui/core/ui_element.py index c36d4257..6ba9eaef 100644 --- a/pygame_gui/core/ui_element.py +++ b/pygame_gui/core/ui_element.py @@ -44,17 +44,26 @@ def __init__(self, relative_rect: Union[pygame.Rect, Tuple[int, int, int, int]], self._layer = 0 self.ui_manager = manager + self.ui_container = None if self.ui_manager is None: self.ui_manager = get_default_manager() if self.ui_manager is None: raise ValueError("Need to create at least one UIManager to create UIElements") super().__init__(self.ui_manager.get_sprite_group()) + + self.minimum_dimensions = (-1, -1) + relative_rect.size = self._get_clamped_to_minimum_dimensions(relative_rect.size) + if isinstance(relative_rect, pygame.Rect): self.relative_rect = relative_rect.copy() else: self.relative_rect = pygame.Rect(relative_rect) self.rect = self.relative_rect.copy() + + self.dynamic_width = True if self.relative_rect.width == -1 else False + self.dynamic_height = True if self.relative_rect.height == -1 else False + self.ui_group = self.ui_manager.get_sprite_group() self.ui_theme = self.ui_manager.get_theme() @@ -139,7 +148,6 @@ def __init__(self, relative_rect: Union[pygame.Rect, Tuple[int, int, int, int]], self.border_width = None # type: Union[None, int] self.shape_corner_radius = None # type: Union[None, int] - self.ui_container = None self._setup_container(container) self.dirty = 1 @@ -152,6 +160,26 @@ def __init__(self, relative_rect: Union[pygame.Rect, Tuple[int, int, int, int]], self._focus_set = {self} + def _get_clamped_to_minimum_dimensions(self, dimensions, clamp_to_container=False): + if self.ui_container is not None and clamp_to_container: + dimensions = (min(self.ui_container.rect.width, + max(self.minimum_dimensions[0], + int(dimensions[0]))), + min(self.ui_container.rect.height, + max(self.minimum_dimensions[1], + int(dimensions[1])))) + else: + dimensions = (max(self.minimum_dimensions[0], int(dimensions[0])), + max(self.minimum_dimensions[1], int(dimensions[1]))) + return dimensions + + def _on_contents_changed(self): + if self.dynamic_width or self.dynamic_height: + self._calc_dynamic_size() + + def _calc_dynamic_size(self): + pass + @staticmethod def _validate_horizontal_anchors(anchors: Dict[str, Union[str, 'UIElement']]): # first make a dictionary of just the horizontal anchors @@ -462,6 +490,7 @@ def _update_absolute_rect_position_from_anchors(self, recalculate_margins=False) self.rect.top = new_top new_height = new_bottom - new_top new_width = new_right - new_left + new_width, new_height = self._get_clamped_to_minimum_dimensions((new_width, new_height)) if (new_height != self.relative_rect.height) or (new_width != self.relative_rect.width): self.set_dimensions((new_width, new_height)) @@ -649,9 +678,30 @@ def set_position(self, position: Union[pygame.math.Vector2, self._update_container_clip() self.ui_container.on_anchor_target_changed(self) + def set_minimum_dimensions(self, dimensions: Union[pygame.math.Vector2, + Tuple[int, int], + Tuple[float, float]]): + """ + If this window is resizable, then the dimensions we set here will be the minimum that + users can change the window to. They are also used as the minimum size when + 'set_dimensions' is called. + + :param dimensions: The new minimum dimension for the window. + + """ + self.minimum_dimensions = (min(self.ui_container.rect.width, int(dimensions[0])), + min(self.ui_container.rect.height, int(dimensions[1]))) + + if ((self.rect.width < self.minimum_dimensions[0]) or + (self.rect.height < self.minimum_dimensions[1])): + new_width = max(self.minimum_dimensions[0], self.rect.width) + new_height = max(self.minimum_dimensions[1], self.rect.height) + self.set_dimensions((new_width, new_height)) + def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], - Tuple[float, float]]): + Tuple[float, float]], + clamp_to_container: bool = False): """ Method to directly set the dimensions of an element. @@ -659,8 +709,11 @@ def set_dimensions(self, dimensions: Union[pygame.math.Vector2, may make a mess of them. :param dimensions: The new dimensions to set. + :param clamp_to_container: Whether we should clamp the dimensions to the + dimensions of the container or not. """ + dimensions = self._get_clamped_to_minimum_dimensions(dimensions, clamp_to_container) self.relative_rect.width = int(dimensions[0]) self.relative_rect.height = int(dimensions[1]) self.rect.size = self.relative_rect.size