Exemplo n.º 1
0
    def test_horiz_center_row(self, _init_pygame,
                              default_ui_manager: UIManager):
        input_data = deque([])
        line_spacing = 1.25
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=line_spacing)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=line_spacing,
                                      layout=text_box_layout)

        simple_rect = SimpleTestLayoutRect(dimensions=(20, 30))
        simple_rect_2 = SimpleTestLayoutRect(dimensions=(50, 30))
        simple_rect_3 = SimpleTestLayoutRect(dimensions=(30, 30))
        layout_row.add_item(simple_rect)
        layout_row.add_item(simple_rect_2)
        layout_row.add_item(simple_rect_3)

        layout_row.horiz_center_row(text_box_layout.floating_rects)

        assert layout_row.left == 50
        assert layout_row.items[0].x == 50
        assert layout_row.items[1].x == 70
        assert layout_row.items[2].x == 120
        assert layout_row.items[2].right == 150
        assert layout_row.right == 150
Exemplo n.º 2
0
    def test_insert_text(self):
        input_data = deque([])
        line_spacing = 1.25
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=line_spacing)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=line_spacing,
                                      layout=text_box_layout)

        font_1 = pygame.freetype.Font(None, 30)
        font_1.origin = True
        font_1.pad = True
        text_chunk_1 = TextLineChunkFTFont(text='test',
                                           font=font_1,
                                           underlined=False,
                                           colour=pygame.Color('#FFFFFF'),
                                           using_default_text_colour=False,
                                           bg_colour=pygame.Color('#00000000'))

        layout_row.add_item(text_chunk_1)

        layout_row.insert_text(' this', 4)

        assert layout_row.items[0].text == 'test this'
Exemplo n.º 3
0
    def test_rewind_row(self, _init_pygame, default_ui_manager: UIManager):
        input_data = deque([])
        line_spacing = 1.25
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=line_spacing)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=line_spacing,
                                      layout=text_box_layout)

        simple_rect = SimpleTestLayoutRect(dimensions=(200, 30))
        simple_rect_2 = SimpleTestLayoutRect(dimensions=(100, 30))
        simple_rect_3 = SimpleTestLayoutRect(dimensions=(80, 30))
        layout_row.add_item(simple_rect)
        layout_row.add_item(simple_rect_2)
        layout_row.add_item(simple_rect_3)

        rewound_data = deque([])
        layout_row.rewind_row(rewound_data)

        assert rewound_data.pop().width == simple_rect_3.width
        assert rewound_data.pop().width == simple_rect_2.width
        assert rewound_data.pop().width == simple_rect.width
Exemplo n.º 4
0
    def test_clear(self, _init_pygame, default_ui_manager: UIManager):
        input_data = deque([])
        line_spacing = 1.25
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=line_spacing)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=line_spacing,
                                      layout=text_box_layout)

        font_1 = pygame.freetype.Font(None, 30)
        font_1.origin = True
        font_1.pad = True
        text_chunk_1 = TextLineChunkFTFont(text='D',
                                           font=font_1,
                                           underlined=False,
                                           colour=pygame.Color('#FFFFFF'),
                                           using_default_text_colour=False,
                                           bg_colour=pygame.Color('#00000000'))

        layout_row.add_item(text_chunk_1)
        layout_surface = pygame.Surface((200, 300),
                                        depth=32,
                                        flags=pygame.SRCALPHA)
        layout_surface.fill((0, 0, 0, 0))
        layout_row.finalise(layout_surface)
        assert layout_surface.get_at((18, 18)) == pygame.Color('#FFFFFF')

        layout_row.clear()
        assert layout_surface.get_at((18, 18)) == pygame.Color('#00000000')
Exemplo n.º 5
0
    def test_vert_align_items_to_row(self, _init_pygame,
                                     default_ui_manager: UIManager):
        input_data = deque([])
        line_spacing = 1.25
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=line_spacing)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=line_spacing,
                                      layout=text_box_layout)

        font_1 = pygame.freetype.Font(None, 30)
        font_2 = pygame.freetype.Font(None, 20)
        font_3 = pygame.freetype.Font(None, 10)
        text_chunk_1 = TextLineChunkFTFont(text='hello ',
                                           font=font_1,
                                           underlined=False,
                                           colour=pygame.Color('#FFFFFF'),
                                           using_default_text_colour=False,
                                           bg_colour=pygame.Color('#FF0000'))
        text_chunk_2 = TextLineChunkFTFont(text='this is a',
                                           font=font_2,
                                           underlined=False,
                                           colour=pygame.Color('#FFFFFF'),
                                           using_default_text_colour=False,
                                           bg_colour=pygame.Color('#FF0000'))
        text_chunk_3 = TextLineChunkFTFont(text=' test',
                                           font=font_3,
                                           underlined=False,
                                           colour=pygame.Color('#FFFFFF'),
                                           using_default_text_colour=False,
                                           bg_colour=pygame.Color('#FF0000'))
        layout_row.add_item(text_chunk_1)
        layout_row.add_item(text_chunk_2)
        layout_row.add_item(text_chunk_3)

        # not sure this is right, need to do some more visual testing of vertical
        # alignment of text rects with different height text on a single row.
        assert layout_row.items[0].y == 0
        assert layout_row.items[1].y == 7
        assert layout_row.items[2].y == 15

        layout_row.vert_align_items_to_row()

        assert layout_row.items[0].y == 0
        assert layout_row.items[1].y == 7
        assert layout_row.items[2].y == 15
Exemplo n.º 6
0
    def test_creation(self, _init_pygame, default_ui_manager: UIManager):
        input_data = deque([])
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=1.0)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=1.25,
                                      layout=text_box_layout)

        assert layout_row.width == 0
        assert layout_row.height == 0
Exemplo n.º 7
0
    def test_set_cursor_position(self):
        input_data = deque([])
        line_spacing = 1.25
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=line_spacing)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=line_spacing,
                                      layout=text_box_layout)

        font_1 = pygame.freetype.Font(None, 30)
        font_1.origin = True
        font_1.pad = True
        text_chunk_1 = TextLineChunkFTFont(text='test',
                                           font=font_1,
                                           underlined=False,
                                           colour=pygame.Color('#FFFFFF'),
                                           using_default_text_colour=False,
                                           bg_colour=pygame.Color('#00000000'))

        layout_row.add_item(text_chunk_1)

        layout_surface = pygame.Surface((200, 300),
                                        depth=32,
                                        flags=pygame.SRCALPHA)
        layout_surface.fill((0, 0, 0, 0))
        layout_row.finalise(layout_surface)
        layout_row.toggle_cursor()

        assert layout_row.edit_cursor_active is True
        assert layout_surface.get_at((1, 5)) == pygame.Color('#FFFFFF')
        assert layout_row.cursor_index == 0
        assert layout_row.cursor_draw_width == 0

        layout_row.set_cursor_position(3)

        layout_row.toggle_cursor()
        layout_row.toggle_cursor()

        assert layout_row.edit_cursor_active is True
        assert layout_surface.get_at((1, 5)) == pygame.Color('#00000000')
        assert layout_row.cursor_index == 3
        assert layout_row.cursor_draw_width == 44
Exemplo n.º 8
0
    def test_at_start(self, _init_pygame, default_ui_manager: UIManager):
        input_data = deque([])
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=1.0)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=1.25,
                                      layout=text_box_layout)

        assert layout_row.at_start()
        simple_rect = SimpleTestLayoutRect(dimensions=(200, 30))
        layout_row.add_item(simple_rect)
        assert not layout_row.at_start()
Exemplo n.º 9
0
    def test_merge_adjacent_compatible_chunks(self, _init_pygame,
                                              default_ui_manager: UIManager):
        input_data = deque([])
        line_spacing = 1.25
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=line_spacing)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=line_spacing,
                                      layout=text_box_layout)

        font_1 = pygame.freetype.Font(None, 30)
        font_2 = pygame.freetype.Font(None, 20)
        text_chunk_1 = TextLineChunkFTFont(text='hello ',
                                           font=font_1,
                                           underlined=False,
                                           colour=pygame.Color('#FFFFFF'),
                                           using_default_text_colour=False,
                                           bg_colour=pygame.Color('#FF0000'))
        text_chunk_2 = TextLineChunkFTFont(text='this is a',
                                           font=font_1,
                                           underlined=False,
                                           colour=pygame.Color('#FFFFFF'),
                                           using_default_text_colour=False,
                                           bg_colour=pygame.Color('#FF0000'))
        text_chunk_3 = TextLineChunkFTFont(text=' test',
                                           font=font_2,
                                           underlined=False,
                                           colour=pygame.Color('#FFFFFF'),
                                           using_default_text_colour=False,
                                           bg_colour=pygame.Color('#FF0000'))
        layout_row.add_item(text_chunk_1)
        layout_row.add_item(text_chunk_2)
        layout_row.add_item(text_chunk_3)

        assert len(layout_row.items) == 3

        layout_row.merge_adjacent_compatible_chunks()

        assert len(layout_row.items) == 2
Exemplo n.º 10
0
    def test_add_item(self, _init_pygame, default_ui_manager: UIManager):
        input_data = deque([])
        line_spacing = 1.25
        text_box_layout = TextBoxLayout(input_data_queue=input_data,
                                        layout_rect=pygame.Rect(
                                            0, 0, 200, 300),
                                        view_rect=pygame.Rect(0, 0, 200, 150),
                                        line_spacing=line_spacing)
        layout_row = TextBoxLayoutRow(row_start_x=0,
                                      row_start_y=0,
                                      row_index=0,
                                      line_spacing=line_spacing,
                                      layout=text_box_layout)

        simple_rect = SimpleTestLayoutRect(dimensions=(200, 30))
        layout_row.add_item(simple_rect)

        assert len(layout_row.items) == 1
        assert layout_row.width == simple_rect.width
        assert layout_row.text_chunk_height == simple_rect.height
        assert layout_row.height == int(simple_rect.height * line_spacing)
Exemplo n.º 11
0
    def build_text_layout(self):
        """
        Build a text box layout for this drawable shape if it has some text.
        """
        containing_rect_when_text_built = self.containing_rect.copy()
        # Draw any text
        if 'text' in self.theming and 'font' in self.theming and self.theming[
                'text'] is not None:
            # we need two rectangles for the text. One is has actual area the
            # text surface takes up, which may be larger than the displayed area,
            # and its position on the final surface. The other is the amount of
            # area of the text surface which we blit from, which may be much smaller
            # than the total text area.

            horiz_padding = 0
            if 'text_horiz_alignment_padding' in self.theming:
                horiz_padding = self.theming['text_horiz_alignment_padding']

            vert_padding = 0
            if 'text_vert_alignment_padding' in self.theming:
                vert_padding = self.theming['text_vert_alignment_padding']

            total_text_buffer = self.shadow_width + self.border_width + self.rounded_corner_offset
            self.text_view_rect = self.containing_rect.copy()
            self.text_view_rect.x = 0
            self.text_view_rect.y = 0
            if self.text_view_rect.width != -1:
                self.text_view_rect.width -= ((total_text_buffer * 2) +
                                              (2 * horiz_padding))
            if self.text_view_rect.height != -1:
                self.text_view_rect.height -= ((total_text_buffer * 2) +
                                               (2 * vert_padding))

            text_actual_area_rect = self.text_view_rect.copy()
            text_actual_area_rect.x = total_text_buffer + horiz_padding
            text_actual_area_rect.y = total_text_buffer + vert_padding
            if 'text_width' in self.theming:
                text_actual_area_rect.width = self.theming['text_width']
            if 'text_height' in self.theming:
                text_actual_area_rect.height = self.theming['text_height']

            text_shadow_data = (0, 0, 0, pygame.Color('#10101070'), False)
            if 'text_shadow' in self.theming:
                text_shadow_data = self.theming['text_shadow']
            text_chunk = TextLineChunkFTFont(
                self.theming['text'],
                self.theming['font'],
                underlined=False,
                colour=pygame.Color('#FFFFFFFF'),
                using_default_text_colour=True,
                bg_colour=pygame.Color('#00000000'),
                text_shadow_data=text_shadow_data,
                max_dimensions=(text_actual_area_rect.width,
                                text_actual_area_rect.height))
            text_chunk.should_centre_from_baseline = True
            self.text_box_layout = TextBoxLayout(deque([text_chunk]),
                                                 text_actual_area_rect,
                                                 self.text_view_rect,
                                                 line_spacing=1.25)
            if 'text_cursor_colour' in self.theming:
                self.text_box_layout.set_cursor_colour(
                    self.theming['text_cursor_colour'])
            self.align_all_text_rows()
        return containing_rect_when_text_built
Exemplo n.º 12
0
class DrawableShape:
    """
    Base class for a graphical 'shape' that we can use for many different UI elements. The intent
    is to make it easy to switch between UI elements having normal rectangles, circles or rounded
    rectangles as their visual shape while having the same non-shape related functionality.

    :param containing_rect: The rectangle which this shape is entirely contained within (including
                            shadows, borders etc)
    :param theming_parameters: A dictionary of user supplied data that alters the appearance of
                               the shape.
    :param states: Names for the different states the shape can be in, each may have different
                   sets of colours & images.
    :param manager: The UI manager for this UI.

    """
    def __init__(self, containing_rect: pygame.Rect, theming_parameters: Dict,
                 states: List[str], manager: IUIManagerInterface):

        self.theming = theming_parameters
        self.containing_rect = containing_rect.copy()
        self.text_view_rect: Optional[pygame.Rect] = None

        self.shadow_width = 0
        self.border_width = 0
        self.shape_corner_radius = 0
        self.rounded_corner_offset = 0
        if 'shadow_width' in self.theming:
            self.shadow_width = self.theming['shadow_width']
        if 'border_width' in self.theming:
            self.border_width = self.theming['border_width']
        if 'shape_corner_radius' in self.theming:
            self.shape_corner_radius = self.theming['shape_corner_radius']
            self.rounded_corner_offset = int(self.shape_corner_radius -
                                             (math.sin(math.pi / 4) *
                                              self.shape_corner_radius))

        self.text_box_layout: Optional[TextBoxLayout] = None
        self.build_text_layout()

        self._evaluate_contents_for_containing_rect()
        self.containing_rect.width = max(self.containing_rect.width, 1)
        self.containing_rect.height = max(self.containing_rect.height, 1)

        self.initial_text_layout_size = (self.containing_rect.width,
                                         self.containing_rect.height)

        self.states = {}
        for state in states:
            self.states[state] = DrawableShapeState(state)

        if 'normal' in states:
            self.active_state = self.states['normal']
        else:
            raise NotImplementedError(
                "No 'normal' state id supplied for drawable shape")

        self.previous_state: Optional[DrawableShapeState] = None

        if 'transitions' in self.theming:
            self.state_transition_times = self.theming['transitions']
        else:
            self.state_transition_times = {}

        self.ui_manager = manager
        self.shape_cache = self.ui_manager.get_theme().shape_cache

        self.states_to_redraw_queue = deque([])
        self.need_to_clean_up = True

        self.should_trigger_full_rebuild = True
        self.time_until_full_rebuild_after_changing_size = 0.35
        self.full_rebuild_countdown = self.time_until_full_rebuild_after_changing_size

        self.click_area_shape = self.containing_rect.copy()
        self.border_rect = self.containing_rect.copy()
        self.background_rect = self.containing_rect.copy()
        self.base_surface: Optional[pygame.Surface] = None

        self.only_text_changed = False

    def _evaluate_contents_for_containing_rect(self):
        if self.containing_rect.width == -1:
            # check to see if we have text and a font, this won't work with HTML
            # text - throw a warning?
            # What we really need to to is process the html text layout by this
            # point but hold off finalising and passing default colours until later?
            if self.text_box_layout is not None:
                text_width = self.text_box_layout.layout_rect.width

                horiz_padding = 0
                if 'text_horiz_alignment_padding' in self.theming:
                    horiz_padding = self.theming[
                        'text_horiz_alignment_padding']

                # As well as the text width we want to throw in the borders,
                # shadows and any text padding
                final_width = (text_width + (2 * self.shadow_width) +
                               (2 * self.border_width) +
                               (2 * self.rounded_corner_offset) +
                               (2 * horiz_padding))

                self.text_view_rect.width = text_width
                self.text_box_layout.view_rect.width = self.text_view_rect.width
                self.containing_rect.width = final_width
        if self.containing_rect.height == -1:
            if self.text_box_layout is not None:
                text_height = self.text_box_layout.layout_rect.height

                vert_padding = 0
                if 'text_vert_alignment_padding' in self.theming:
                    vert_padding = self.theming['text_vert_alignment_padding']

                # As well as the text height we want to throw in the borders,
                # shadows and any text padding
                final_height = (text_height + (2 * self.shadow_width) +
                                (2 * self.border_width) +
                                (2 * self.rounded_corner_offset) +
                                (2 * vert_padding))
                self.text_view_rect.height = text_height
                self.text_box_layout.view_rect.height = self.text_view_rect.height
                self.containing_rect.height = final_height

    def set_active_state(self, state_id: str):
        """
        Changes the currently active state for the drawable shape and, if setup in the theme,
        creates a transition blend from the previous state to the newly active one.

        :param state_id: the ID of the new state to make active.

        """

        # make sure this state is generated before we set it.
        # should ensure that some more rarely used states are only generated if we use them
        if not self.states[state_id].generated:
            if state_id in self.states_to_redraw_queue:
                self.states_to_redraw_queue.remove(state_id)
            self.redraw_state(state_id)

        if state_id in self.states and self.active_state.state_id != state_id:
            self.previous_state = self.active_state
            self.active_state = self.states[state_id]
            self.active_state.has_fresh_surface = True

            if self.previous_state is not None and (
                (self.previous_state.state_id, self.active_state.state_id)
                    in self.state_transition_times):
                prev_id = self.previous_state.state_id
                next_id = self.active_state.state_id
                duration = self.state_transition_times[(
                    self.previous_state.state_id, self.active_state.state_id)]
                if self.previous_state.transition is None:
                    # completely fresh transition
                    self.active_state.transition = DrawableStateTransition(
                        self.states, prev_id, next_id, duration)
                else:
                    # check to see if we are reversing an in-progress transition.
                    if self.previous_state.transition.start_stat_id == self.active_state.state_id:
                        progress_time = self.previous_state.transition.remaining_time
                        transition = DrawableStateTransition(
                            self.states,
                            prev_id,
                            next_id,
                            duration,
                            progress=progress_time)
                        self.active_state.transition = transition

    def update(self, time_delta: float):
        """
        Updates the drawable shape to process rebuilds and update blends between states.

        :param time_delta: amount of time passed between now and the previous frame in seconds.

        """
        if len(self.states_to_redraw_queue) > 0:
            state = self.states_to_redraw_queue.popleft()
            self.redraw_state(state)
        if self.need_to_clean_up and len(self.states_to_redraw_queue) == 0:
            # last state so clean up
            self.clean_up_temp_shapes()
            self.need_to_clean_up = False

        if self.full_rebuild_countdown > 0.0:
            self.full_rebuild_countdown -= time_delta

        if self.should_trigger_full_rebuild and self.full_rebuild_countdown <= 0.0:
            self.full_rebuild_on_size_change()

        self.active_state.update(time_delta)

    def full_rebuild_on_size_change(self):
        """
        Triggered when we've changed the size of the shape and need to rebuild basically everything
        to account for it.

        """
        shape_params_changed = False
        if 'shadow_width' in self.theming and self.shadow_width != self.theming[
                'shadow_width']:
            self.shadow_width = self.theming['shadow_width']
            shape_params_changed = True
        if 'border_width' in self.theming and self.border_width != self.theming[
                'border_width']:
            self.border_width = self.theming['border_width']
            shape_params_changed = True
        if ('shape_corner_radius' in self.theming and self.shape_corner_radius
                != self.theming['shape_corner_radius']):
            self.shape_corner_radius = self.theming['shape_corner_radius']
            self.rounded_corner_offset = int(self.shape_corner_radius -
                                             (math.sin(math.pi / 4) *
                                              self.shape_corner_radius))
            shape_params_changed = True

        if shape_params_changed or self.initial_text_layout_size != self.containing_rect.size:
            self.build_text_layout()
        self.should_trigger_full_rebuild = False
        self.full_rebuild_countdown = self.time_until_full_rebuild_after_changing_size

    def redraw_all_states(self):
        """
        Starts the redrawing process for all states of this shape that auto pre-generate.
        Redrawing is done one state at a time so will take a few loops of the game to
        complete if this shape has many states.
        """
        self.states_to_redraw_queue = deque([
            state_id for state_id, state in self.states.items()
            if state.should_auto_pregen
        ])
        initial_state = self.states_to_redraw_queue.popleft()
        self.redraw_state(initial_state)

    def align_all_text_rows(self):
        """
        Aligns the text drawing position correctly according to our theming options.

        """
        # Horizontal alignment
        if 'text_horiz_alignment' in self.theming:
            if (self.theming['text_horiz_alignment'] == 'center'
                    or self.theming['text_horiz_alignment']
                    not in ['left', 'right']):
                method = 'rect'
                if 'text_horiz_alignment_method' in self.theming:
                    method = self.theming['text_horiz_alignment_method']
                self.text_box_layout.horiz_center_all_rows(method)
            elif self.theming['text_horiz_alignment'] == 'left':
                self.text_box_layout.align_left_all_rows(0)
            else:
                self.text_box_layout.align_right_all_rows(0)
        else:
            method = 'rect'
            if 'text_horiz_alignment_method' in self.theming:
                method = self.theming['text_horiz_alignment_method']
            self.text_box_layout.horiz_center_all_rows(method)

        # Vertical alignment
        if 'text_vert_alignment' in self.theming:
            if (self.theming['text_vert_alignment'] == 'center'
                    or self.theming['text_vert_alignment']
                    not in ['top', 'bottom']):
                self.text_box_layout.vert_center_all_rows()
            elif self.theming['text_vert_alignment'] == 'top':
                self.text_box_layout.vert_align_top_all_rows(0)
            else:
                self.text_box_layout.vert_align_bottom_all_rows(0)
        else:
            self.text_box_layout.vert_center_all_rows()

    def get_active_state_surface(self) -> pygame.surface.Surface:
        """
        Get the main surface from the active state.

        :return: The surface asked for, or the best available substitute.

        """
        if self.active_state is not None:
            return self.active_state.get_surface()
        else:
            return self.ui_manager.get_universal_empty_surface()

    def get_surface(self, state_name: str) -> pygame.surface.Surface:
        """
        Get the main surface from a specific state.

        :param state_name: The state we are trying to get the surface from.

        :return: The surface asked for, or the best available substitute.

        """
        if state_name in self.states and self.states[
                state_name].surface is not None:
            return self.states[state_name].surface
        elif state_name in self.states and self.states[
                'normal'].surface is not None:
            return self.states['normal'].surface
        else:
            return pygame.surface.Surface((0, 0),
                                          flags=pygame.SRCALPHA,
                                          depth=32)

    def get_fresh_surface(self) -> pygame.surface.Surface:
        """
        Gets the surface of the active state and resets the state's 'has_fresh_surface' variable.

        :return: The active state's main pygame.surface.Surface.

        """
        self.active_state.has_fresh_surface = False
        return self.get_active_state_surface()

    def has_fresh_surface(self) -> bool:
        """
        Lets UI elements find out when a state has finished building a fresh surface for times
        when we have to delay it for whatever reason.

        :return: True if there is a freshly built surface waiting, False if the shape has not
                 changed.

        """
        return self.active_state.has_fresh_surface

    def finalise_images_and_text(self, image_state_str: str, state_str: str,
                                 text_colour_state_str: str,
                                 text_shadow_colour_state_str: str,
                                 add_text: bool):
        """
        Rebuilds any text or image used by a specific state in the drawable shape. Effectively
        this means adding them on top of whatever is already in the state's surface. As such it
        should generally be called last in the process of building up a finished drawable shape
        state.

        :param add_text:
        :param image_state_str: image ID of the state we are going to be adding images and text to.
        :param state_str: normal ID of the state we are going to be adding images and text to.
        :param text_colour_state_str: text ID of the state we are going to be adding images and
                                      text to.
        :param text_shadow_colour_state_str: text shadow ID of the state we are going to be adding
                                             images and text to.

        """
        # Draw any themed images
        if image_state_str in self.theming and self.theming[
                image_state_str] is not None:
            image_rect = self.theming[image_state_str].get_rect()
            image_rect.center = (int(self.containing_rect.width / 2),
                                 int(self.containing_rect.height / 2))
            basic_blit(self.states[state_str].surface,
                       self.theming[image_state_str], image_rect)
        if add_text:
            self.finalise_text(state_str, text_colour_state_str,
                               text_shadow_colour_state_str)

    def build_text_layout(self):
        """
        Build a text box layout for this drawable shape if it has some text.
        """
        containing_rect_when_text_built = self.containing_rect.copy()
        # Draw any text
        if 'text' in self.theming and 'font' in self.theming and self.theming[
                'text'] is not None:
            # we need two rectangles for the text. One is has actual area the
            # text surface takes up, which may be larger than the displayed area,
            # and its position on the final surface. The other is the amount of
            # area of the text surface which we blit from, which may be much smaller
            # than the total text area.

            horiz_padding = 0
            if 'text_horiz_alignment_padding' in self.theming:
                horiz_padding = self.theming['text_horiz_alignment_padding']

            vert_padding = 0
            if 'text_vert_alignment_padding' in self.theming:
                vert_padding = self.theming['text_vert_alignment_padding']

            total_text_buffer = self.shadow_width + self.border_width + self.rounded_corner_offset
            self.text_view_rect = self.containing_rect.copy()
            self.text_view_rect.x = 0
            self.text_view_rect.y = 0
            if self.text_view_rect.width != -1:
                self.text_view_rect.width -= ((total_text_buffer * 2) +
                                              (2 * horiz_padding))
            if self.text_view_rect.height != -1:
                self.text_view_rect.height -= ((total_text_buffer * 2) +
                                               (2 * vert_padding))

            text_actual_area_rect = self.text_view_rect.copy()
            text_actual_area_rect.x = total_text_buffer + horiz_padding
            text_actual_area_rect.y = total_text_buffer + vert_padding
            if 'text_width' in self.theming:
                text_actual_area_rect.width = self.theming['text_width']
            if 'text_height' in self.theming:
                text_actual_area_rect.height = self.theming['text_height']

            text_shadow_data = (0, 0, 0, pygame.Color('#10101070'), False)
            if 'text_shadow' in self.theming:
                text_shadow_data = self.theming['text_shadow']
            text_chunk = TextLineChunkFTFont(
                self.theming['text'],
                self.theming['font'],
                underlined=False,
                colour=pygame.Color('#FFFFFFFF'),
                using_default_text_colour=True,
                bg_colour=pygame.Color('#00000000'),
                text_shadow_data=text_shadow_data,
                max_dimensions=(text_actual_area_rect.width,
                                text_actual_area_rect.height))
            text_chunk.should_centre_from_baseline = True
            self.text_box_layout = TextBoxLayout(deque([text_chunk]),
                                                 text_actual_area_rect,
                                                 self.text_view_rect,
                                                 line_spacing=1.25)
            if 'text_cursor_colour' in self.theming:
                self.text_box_layout.set_cursor_colour(
                    self.theming['text_cursor_colour'])
            self.align_all_text_rows()
        return containing_rect_when_text_built

    def finalise_text(self,
                      state_str,
                      text_colour_state_str: str = "",
                      text_shadow_colour_state_str: str = "",
                      only_text_changed: bool = False):
        """
        Finalise the text to a surface with some last-minute data that doesn't require the text
        be re-laid out.

        :param only_text_changed:
        :param state_str: The name of the shape's state we are finalising.
        :param text_colour_state_str: The string identifying the text colour to use.
        :param text_shadow_colour_state_str: The string identifying the text shadow
                                             colour to use.
        """
        if self.text_box_layout is not None:
            # copy the pre-text surface & create a new empty text surface for this state
            self.states[state_str].pre_text_surface = self.states[
                state_str].surface.copy()
            self.states[state_str].text_surface = pygame.surface.Surface(
                self.states[state_str].surface.get_size(),
                flags=pygame.SRCALPHA,
                depth=32)
            self.states[state_str].text_surface.fill('#00000000')

            if only_text_changed:

                self.text_box_layout.blit_finalised_text_to_surf(
                    self.states[state_str].text_surface)
                basic_blit(self.states[state_str].surface,
                           self.states[state_str].text_surface, (0, 0))
            else:
                self.text_box_layout.set_default_text_colour(
                    self.theming[text_colour_state_str])
                self.text_box_layout.set_default_text_shadow_colour(
                    self.theming[text_shadow_colour_state_str])
                self.text_box_layout.finalise_to_surf(
                    self.states[state_str].text_surface)
                basic_blit(self.states[state_str].surface,
                           self.states[state_str].text_surface, (0, 0))

    def apply_active_text_changes(self):
        """
        Updates the shape surface with any changes to the text surface. Useful when we've made
        small edits to the text surface
        """
        if self.text_box_layout is not None:
            for state_id, state in self.states.items():
                if state.pre_text_surface is not None and state.text_surface is not None:
                    state.surface = state.pre_text_surface.copy()
                    basic_blit(state.surface, state.text_surface, (0, 0))

    def set_text(self, text: str):
        """
        Set the visible text that the drawable shape has on it. This call will build a text
        layout and then redraw the final shape with the new, laid out text on top.

        :param text: the new string of text to stick on the shape.
        """
        self.theming['text'] = text
        self.build_text_layout()
        self.redraw_all_states()

    def set_text_alpha(self, alpha: int):
        """
        Set the alpha of just the text and redraw the shape with the new text on top.

        :param alpha: the alpha to set.
        """
        self.text_box_layout.set_alpha(alpha)
        self.redraw_state(self.active_state.state_id, add_text=False)
        self.finalise_text(self.active_state.state_id, only_text_changed=True)

    def redraw_active_state_no_text(self):
        """
        Redraw the currently active state with no text.
        """
        self.redraw_state(self.active_state.state_id, add_text=False)

    def finalise_text_onto_active_state(self):
        """
        Lets us draw the active state with no text and then paste the finalised surface from the
        text layout on top. Handy if we are doing some text effects in the text layout we don't want
        to lose by recreating the text from scratch.
        """
        self.redraw_state(self.active_state.state_id, add_text=False)
        self.finalise_text(self.active_state.state_id, only_text_changed=True)

    def insert_text(self,
                    text: str,
                    layout_index: int,
                    parser: Optional[HTMLParser] = None):
        """
        Update the theming when we insert text, then pass down to the layout to do the actual
        inserting.
        :param text: the text to insert
        :param layout_index: where to insert it
        :param parser: an optional parser
        :return:
        """
        self.theming['text'] = (self.theming['text'][:layout_index] + text +
                                self.theming['text'][layout_index:])
        self.text_box_layout.insert_text(text, layout_index, parser)

    def toggle_text_cursor(self):
        """
        Toggle the edit text cursor/carat between visible and invisible. Usually this is run to
        make the cursor appear to flash so it catches user attention.
        """
        if self.text_box_layout is not None:
            self.text_box_layout.toggle_cursor()
            self.apply_active_text_changes()
            self.active_state.has_fresh_surface = True

    def redraw_state(self, state_str: str, add_text: bool = True):
        """
        This method is declared for derived classes to implement but has no default
        implementation.

        :param add_text:
        :param state_str: The ID/name of the state to redraw.

        """

    def clean_up_temp_shapes(self):
        """
        This method is declared for derived classes to implement but has no default implementation.
        """

    def collide_point(self, point: Union[pygame.math.Vector2, Tuple[int, int],
                                         Tuple[float, float]]):
        """
        This method is declared for derived classes to implement but has no default implementation.

        :param point: A point to collide with this shape.

        """

    def set_position(self, point: Union[pygame.math.Vector2, Tuple[int, int],
                                        Tuple[float, float]]):
        """
        This method is declared for derived classes to implement but has no default implementation.

        :param point: A point to set this shapes position to.

        """

    def set_dimensions(self, dimensions: Union[pygame.math.Vector2,
                                               Tuple[int, int], Tuple[float,
                                                                      float]]):
        """