Repository: MyreMylar/pygame_gui (language: python)

Issue:
Multiline text entry
There doesn't seem to be a multiline text entry widget which I think would be very useful if it did exist.


Discussion / hints:
I could use it right now :D
Once the current text refactoring is done this should be a lot more possible as I am moving most of the text editing functions to be part of the shared text drawing and layout code that all elements are going to share.

Might be in 0.6.0, or more likely the update afterwards.
Is this implemented yet?  I've looked through the API documentation and I can't find anything, but I might be missing something.
Nope it is not in yet. I started looking at it for 0.6.0 but was way too busy last year to give it a proper investment of time.

The back end text refactor is done (for now) so it's definitely more possible, just needs a lot of fiddling to make a useable UI element of out of it with a good user experience. This is on my task list to look at again for the summer.

Fix patch:
diff --git a/pygame_gui/core/drawable_shapes/drawable_shape.py b/pygame_gui/core/drawable_shapes/drawable_shape.py
index 8b82f820..2a9b1472 100644
--- a/pygame_gui/core/drawable_shapes/drawable_shape.py
+++ b/pygame_gui/core/drawable_shapes/drawable_shape.py
@@ -516,8 +516,14 @@ def build_text_layout(self):
                                              max_dimensions=(text_actual_area_rect.width,
                                                              text_actual_area_rect.height))
             text_chunk.should_centre_from_baseline = True
+            default_font_data = {"font": self.theming['font'],
+                                 "font_colour": (self.theming['normal_text']
+                                                 if 'normal_text' in self.theming else
+                                                 self.ui_manager.get_theme().get_colour('normal_text', None)),
+                                 "bg_colour": pygame.Color('#00000000')}
             self.text_box_layout = TextBoxLayout(deque([text_chunk]), text_actual_area_rect,
-                                                 self.text_view_rect, line_spacing=1.25)
+                                                 self.text_view_rect, line_spacing=1.25,
+                                                 default_font_data=default_font_data)
             if 'selected_bg' in self.theming:
                 self.text_box_layout.selection_colour = self.theming['selected_bg']
             if 'text_cursor_colour' in self.theming:
diff --git a/pygame_gui/core/text/html_parser.py b/pygame_gui/core/text/html_parser.py
index 71ece98d..d6b15b7d 100644
--- a/pygame_gui/core/text/html_parser.py
+++ b/pygame_gui/core/text/html_parser.py
@@ -160,59 +160,86 @@ def _handle_shadow_tag(self, attributes, style):
         shadow_offset = [0, 0]
         shadow_colour = [50, 50, 50]
         if 'size' in attributes:
-            shadow_size = int(attributes['size'])
+            if attributes['size'] is not None and len(attributes['size']) > 0:
+                try:
+                    shadow_size = int(attributes['size'])
+                except ValueError or AttributeError:
+                    shadow_size = 0
+
         if 'offset' in attributes:
-            offset_str = attributes['offset'].split(',')
-            shadow_offset[0] = int(offset_str[0])
-            shadow_offset[1] = int(offset_str[1])
+            if attributes['offset'] is not None and len(attributes['offset']) > 0:
+                try:
+                    offset_str = attributes['offset'].split(',')
+                    if len(offset_str) == 2:
+                        shadow_offset[0] = int(offset_str[0])
+                        shadow_offset[1] = int(offset_str[1])
+                except ValueError or AttributeError:
+                    shadow_offset = [0, 0]
         if 'color' in attributes:
-            if attributes['color'][0] == '#':
-                shadow_colour = pygame.color.Color(attributes['color'])
-            else:
-                shadow_colour = self.ui_theme.get_colour_or_gradient(attributes['color'],
-                                                                     self.combined_ids)
+            try:
+                if self._is_legal_hex_colour(attributes['color']):
+                    shadow_colour = pygame.color.Color(attributes['color'])
+                elif attributes['color'] is not None and len(attributes['color']) > 0:
+                    shadow_colour = self.ui_theme.get_colour_or_gradient(attributes['color'],
+                                                                         self.combined_ids)
+            except ValueError or AttributeError:
+                shadow_colour = [50, 50, 50]
+
         style['shadow_data'] = (shadow_size, shadow_offset[0],
                                 shadow_offset[1], shadow_colour, False)
 
     def _handle_font_tag(self, attributes, style):
-        if 'face' in attributes:
+        if 'face' in attributes and attributes['face'] is not None:
             font_name = attributes['face'] if len(attributes['face']) > 0 else None
             style["font_name"] = font_name
         if 'pixel_size' in attributes:
-            if len(attributes['pixel_size']) > 0:
-                font_size = int(attributes['pixel_size'])
+            if attributes['pixel_size'] is not None and len(attributes['pixel_size']) > 0:
+                try:
+                    font_size = int(attributes['pixel_size'])
+                except ValueError or AttributeError:
+                    font_size = self.default_style['font_size']
             else:
-                font_size = None
+                font_size = self.default_style['font_size']
             style["font_size"] = font_size
         elif 'size' in attributes:
-            if len(attributes['size']) > 0:
-                if float(attributes['size']) in self.font_sizes:
-                    font_size = self.font_sizes[float(attributes['size'])]
-                else:
-                    warnings.warn('Size of: ' + str(float(attributes['size'])) +
-                                  " - is not a supported html style size."
-                                  " Try .5 increments between 1 & 7 or use 'pixel_size' instead to "
-                                  "set the font size directly", category=UserWarning)
+            if attributes['size'] is not None and len(attributes['size']) > 0:
+                try:
+                    if float(attributes['size']) in self.font_sizes:
+                        font_size = self.font_sizes[float(attributes['size'])]
+                    else:
+                        warnings.warn('Size of: ' + str(float(attributes['size'])) +
+                                      " - is not a supported html style size."
+                                      " Try .5 increments between 1 & 7 or use 'pixel_size' instead to "
+                                      "set the font size directly", category=UserWarning)
+                        font_size = self.default_style['font_size']
+                except ValueError or AttributeError:
                     font_size = self.default_style['font_size']
             else:
-                font_size = None
+                font_size = self.default_style['font_size']
             style["font_size"] = font_size
         if 'color' in attributes:
-            if attributes['color'][0] == '#':
-                style["font_colour"] = pygame.color.Color(attributes['color'])
-            else:
-                style["font_colour"] = self.ui_theme.get_colour_or_gradient(attributes['color'],
-                                                                            self.combined_ids)
+            style["font_colour"] = self.default_style["font_colour"]
+            try:
+                if self._is_legal_hex_colour(attributes['color']):
+                    style["font_colour"] = pygame.color.Color(attributes['color'])
+                elif attributes['color'] is not None and len(attributes['color']) > 0:
+                    style["font_colour"] = self.ui_theme.get_colour_or_gradient(attributes['color'],
+                                                                                self.combined_ids)
+            except ValueError or AttributeError:
+                style["font_colour"] = self.default_style["font_colour"]
 
     def _handle_body_tag(self, attributes, style):
         if 'bgcolor' in attributes:
-            if len(attributes['bgcolor']) > 0:
-                if attributes['bgcolor'][0] == '#':
-                    style["bg_colour"] = pygame.color.Color(attributes['bgcolor'])
-                else:
-                    style["bg_colour"] = self.ui_theme.get_colour_or_gradient(
-                        attributes['bgcolor'],
-                        self.combined_ids)
+            if attributes['bgcolor'] is not None and len(attributes['bgcolor']) > 0:
+                try:
+                    if self._is_legal_hex_colour(attributes['bgcolor']):
+                        style["bg_colour"] = pygame.color.Color(attributes['bgcolor'])
+                    else:
+                        style["bg_colour"] = self.ui_theme.get_colour_or_gradient(
+                            attributes['bgcolor'],
+                            self.combined_ids)
+                except ValueError or AttributeError:
+                    style["bg_colour"] = pygame.Color('#00000000')
             else:
                 style["bg_colour"] = pygame.Color('#00000000')
 
@@ -225,7 +252,9 @@ def _handle_line_break(self):
         dimensions = (current_font.get_rect(' ').width,
                       int(round(self.current_style['font_size'] *
                                 self.line_spacing)))
-        self.layout_rect_queue.append(LineBreakLayoutRect(dimensions=dimensions))
+        chunk = self.create_styled_text_chunk('')
+        self.layout_rect_queue.append(LineBreakLayoutRect(dimensions=dimensions, font=current_font))
+        self.layout_rect_queue.append(chunk)
 
     def _handle_p_tag(self):
         if self.in_paragraph_block:
@@ -248,7 +277,7 @@ def _handle_img_tag(self, attributes):
                 image_float = TextFloatPosition.RIGHT
             else:
                 image_float = TextFloatPosition.NONE
-        if 'padding' in attributes:
+        if 'padding' in attributes and isinstance(attributes['padding'], str):
             paddings = attributes['padding'].split(' ')
             for index, padding in enumerate(paddings):
                 paddings[index] = int(padding.strip('px'))
@@ -272,6 +301,11 @@ def _handle_img_tag(self, attributes):
                 padding_right = paddings[0]
                 padding_left = paddings[0]
                 padding_bottom = paddings[0]
+            else:
+                padding_top = 0
+                padding_right = 0
+                padding_left = 0
+                padding_bottom = 0
         all_paddings = Padding(padding_top, padding_right, padding_bottom, padding_left)
         self.layout_rect_queue.append(ImageLayoutRect(image_path,
                                                       image_float,
@@ -403,3 +437,15 @@ def create_styled_text_chunk(self, text: str):
                                         text_shadow_data=self.current_style['shadow_data'],
                                         effect_id=self.current_style['effect_id'])
         return chunk
+
+    @staticmethod
+    def _is_legal_hex_colour(col):
+        if col is not None and len(col) > 0 and col[0] == '#':
+            if len(col) == 7 or len(col) == 9:
+                for col_index in range(1, len(col)):
+                    col_letter = col[col_index]
+                    if col_letter not in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+                                          'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'E', 'f', 'F']:
+                        return False
+                return True
+        return False
diff --git a/pygame_gui/core/text/line_break_layout_rect.py b/pygame_gui/core/text/line_break_layout_rect.py
index 4b7e12eb..97d2eff4 100644
--- a/pygame_gui/core/text/line_break_layout_rect.py
+++ b/pygame_gui/core/text/line_break_layout_rect.py
@@ -1,7 +1,9 @@
 from typing import Tuple, Optional
 
+import pygame
 from pygame.surface import Surface
 from pygame.rect import Rect
+from pygame import Color
 
 from pygame_gui.core.text.text_layout_rect import TextLayoutRect
 
@@ -13,8 +15,17 @@ class LineBreakLayoutRect(TextLayoutRect):
     :param dimensions: The dimensions of the 'line break', the height is the important thing
                        so the new lines are spaced correctly for the last active font.
     """
-    def __init__(self, dimensions: Tuple[int, int]):
+    def __init__(self, dimensions: Tuple[int, int], font):
         super().__init__(dimensions)
+        self.letter_count = 1
+        self.is_selected = False
+        self.selection_colour = Color(128, 128, 128, 255)
+        self.selection_chunk_width = 4
+        self.select_surf = None
+        self.font = font
+
+    def __repr__(self):
+        return "<LineBreakLayoutRect " + super().__repr__() + " >"
 
     def finalise(self,
                  target_surface: Surface,
@@ -24,4 +35,8 @@ def finalise(self,
                  row_bg_height: int,
                  x_scroll_offset: int = 0,
                  letter_end: Optional[int] = None):
-        pass
+        if self.is_selected:
+            self.select_surf = Surface((self.selection_chunk_width, row_bg_height), flags=pygame.SRCALPHA)
+            self.select_surf.fill(self.selection_colour)
+            target_surface.blit(self.select_surf, self.topleft, special_flags=pygame.BLEND_PREMULTIPLIED)
+
diff --git a/pygame_gui/core/text/text_box_layout.py b/pygame_gui/core/text/text_box_layout.py
index 2747c8ef..01958afd 100644
--- a/pygame_gui/core/text/text_box_layout.py
+++ b/pygame_gui/core/text/text_box_layout.py
@@ -1,4 +1,4 @@
-from typing import Deque, List, Optional
+from typing import Deque, List, Optional, Dict, Any
 from collections import deque
 from bisect import bisect_left
 
@@ -31,11 +31,19 @@ def __init__(self,
                  input_data_queue: Deque[TextLayoutRect],
                  layout_rect: pygame.Rect,
                  view_rect: pygame.Rect,
-                 line_spacing: float):
+                 line_spacing: float,
+                 default_font_data: Dict[str, Any],
+                 allow_split_dashes: bool = True):
         # TODO: supply only a width and create final rect shape or just a final height?
         self.input_data_rect_queue = input_data_queue.copy()
         self.layout_rect = layout_rect.copy()
         self.line_spacing = line_spacing
+
+        # this is the font used when we don't have anything else
+        self.default_font_data = default_font_data
+        self.default_font = default_font_data["font"]
+
+        self.allow_split_dashes = allow_split_dashes
         self.last_row_height = int(14 * self.line_spacing)
 
         self.view_rect = view_rect
@@ -52,6 +60,7 @@ def __init__(self,
         self.floating_rects: List[TextLayoutRect] = []
         self.layout_rows: List[TextBoxLayoutRow] = []
         self.row_lengths = []
+        self.row_lengths_no_end_spaces = []
         self.link_chunks = []
         self.letter_count = 0
         self.current_end_pos = 0
@@ -63,10 +72,21 @@ def __init__(self,
         current_row = TextBoxLayoutRow(row_start_x=self.layout_rect.x,
                                        row_start_y=self.layout_rect.y, row_index=0,
                                        layout=self, line_spacing=self.line_spacing)
-        self._process_layout_queue(self.layout_rect_queue, current_row)
+        if len(self.layout_rect_queue) > 0:
+            self._process_layout_queue(self.layout_rect_queue, current_row)
+        else:
+            empty_text_chunk = TextLineChunkFTFont("",
+                                                   self.default_font,
+                                                   False,
+                                                   self.default_font_data['font_colour'],
+                                                   True,
+                                                   self.default_font_data['bg_colour'])
+            current_row.add_item(empty_text_chunk)
+            self._add_row_to_layout(current_row, True)
 
         self.edit_buffer = 2
         self.cursor_text_row = None
+        self.last_horiz_cursor_row_pos = 0
 
         self.selection_colour = pygame.Color(128, 128, 200, 255)
         self.selected_chunks = []
@@ -128,7 +148,8 @@ def _add_row_to_layout(self, current_row: TextBoxLayoutRow, last_row=False):
         # otherwise we add infinite rows with no height
         # instead add a line break rect to an empty row.
         if len(current_row.items) == 0 and not last_row:
-            current_row.add_item(LineBreakLayoutRect(dimensions=(2, self.last_row_height)))
+            current_row.add_item(LineBreakLayoutRect(dimensions=(2, self.last_row_height),
+                                                     font=current_row.fall_back_font))
         if current_row not in self.layout_rows:
             self.layout_rows.append(current_row)
         if current_row.bottom - self.layout_rect.y > self.layout_rect.height:
@@ -222,36 +243,43 @@ def _add_floating_rect(self, current_row, test_layout_rect, input_queue):
     def _handle_span_rect(self, current_row, test_layout_rect):
         if not current_row.at_start():
             # not at start of line so end current row...
+            prev_row_font = current_row.fall_back_font
             self._add_row_to_layout(current_row)
             # ...and start new one
             current_row = TextBoxLayoutRow(row_start_x=self.layout_rect.x,
                                            row_start_y=current_row.bottom,
                                            row_index=len(self.layout_rows),
                                            layout=self, line_spacing=self.line_spacing)
+            current_row.fall_back_font = prev_row_font
 
         # Make the rect span the current row's full width & add it to the row
         test_layout_rect.width = self.layout_rect.width  # TODO: floating rects?
         current_row.add_item(test_layout_rect)
 
         # add the row to the layout since it's now full up after spanning the full width...
+        prev_row_font = current_row.fall_back_font
         self._add_row_to_layout(current_row)
         # ...then start a new row
         current_row = TextBoxLayoutRow(row_start_x=self.layout_rect.x,
                                        row_start_y=current_row.bottom,
                                        row_index=len(self.layout_rows),
                                        layout=self, line_spacing=self.line_spacing)
+        current_row.fall_back_font = prev_row_font
         return current_row
 
     def _handle_line_break_rect(self, current_row, test_layout_rect):
         # line break, so first end current row...
         current_row.add_item(test_layout_rect)
+        prev_row_font = current_row.fall_back_font
         self._add_row_to_layout(current_row)
 
         # ...then start a new row
-        return TextBoxLayoutRow(row_start_x=self.layout_rect.x,
-                                row_start_y=current_row.bottom,
-                                row_index=len(self.layout_rows),
-                                layout=self, line_spacing=self.line_spacing)
+        new_row = TextBoxLayoutRow(row_start_x=self.layout_rect.x,
+                                   row_start_y=current_row.bottom,
+                                   row_index=len(self.layout_rows),
+                                   layout=self, line_spacing=self.line_spacing)
+        new_row.fall_back_font = prev_row_font
+        return new_row
 
     def _split_rect_and_move_to_next_line(self, current_row, rhs_limit,
                                           test_layout_rect,
@@ -265,7 +293,8 @@ def _split_rect_and_move_to_next_line(self, current_row, rhs_limit,
             try:
                 new_layout_rect = test_layout_rect.split(split_point,
                                                          self.layout_rect.width,
-                                                         self.layout_rect.x)
+                                                         self.layout_rect.x,
+                                                         self.allow_split_dashes)
                 if new_layout_rect is not None:
                     # split successfully...
 
@@ -288,19 +317,22 @@ def _split_rect_and_move_to_next_line(self, current_row, rhs_limit,
             input_queue.appendleft(test_layout_rect)
 
         # whether we split successfully or not, we need to end the current row...
+        prev_row_font = current_row.fall_back_font
         self._add_row_to_layout(current_row)
 
         # And then start a new one.
         if floater_height is not None:
-            return TextBoxLayoutRow(row_start_x=self.layout_rect.x,
-                                    row_start_y=floater_height,
-                                    row_index=len(self.layout_rows),
-                                    layout=self, line_spacing=self.line_spacing)
+            new_row = TextBoxLayoutRow(row_start_x=self.layout_rect.x,
+                                       row_start_y=floater_height,
+                                       row_index=len(self.layout_rows),
+                                       layout=self, line_spacing=self.line_spacing)
         else:
-            return TextBoxLayoutRow(row_start_x=self.layout_rect.x,
-                                    row_start_y=current_row.bottom,
-                                    row_index=len(self.layout_rows),
-                                    layout=self, line_spacing=self.line_spacing)
+            new_row = TextBoxLayoutRow(row_start_x=self.layout_rect.x,
+                                       row_start_y=current_row.bottom,
+                                       row_index=len(self.layout_rows),
+                                       layout=self, line_spacing=self.line_spacing)
+        new_row.fall_back_font = prev_row_font
+        return new_row
 
     def finalise_to_surf(self, surface: Surface):
         """
@@ -419,6 +451,8 @@ def insert_layout_rects(self, layout_rects: Deque[TextLayoutRect],
         Insert some new layout rectangles from a queue at specific place in the current layout.
         Hopefully this means we only need to redo the layout after this point... we shall see.
 
+        Warning: this is a test function, it may noit be up to date with current text layout features
+
         :param layout_rects: the new TextLayoutRects to insert.
         :param row_index: which row we are sticking them on.
         :param item_index: which chunk we are sticking them into.
@@ -436,7 +470,7 @@ def insert_layout_rects(self, layout_rects: Deque[TextLayoutRect],
             row.rewind_row(temp_layout_queue)
 
         self.layout_rows = self.layout_rows[:row_index]
-
+        self._merge_adjacent_compatible_chunks(temp_layout_queue)
         self._process_layout_queue(temp_layout_queue, row)
 
         if self.finalised_surface is not None:
@@ -542,13 +576,72 @@ def set_cursor_position(self, cursor_pos):
                 self.cursor_text_row.toggle_cursor()
             self.cursor_text_row = None
 
-        letter_acc = 0
+        # we figure out how many edit positions there are in the text
+        # each letter counts as one additional position, but so does the start of a new row
+        edit_pos_accumulator = 0
         for row in self.layout_rows:
-            if cursor_pos <= letter_acc + row.letter_count:
-                row.set_cursor_position(cursor_pos - letter_acc)
+            if cursor_pos < edit_pos_accumulator + row.letter_count:
+                row.set_cursor_position(cursor_pos - edit_pos_accumulator)
                 self.cursor_text_row = row
+                break
+            elif cursor_pos == edit_pos_accumulator + row.letter_count:
+                # if the last character in a row is a space, we have more than one row and this isn't the last row
+                # we want to jump to the start of the next row
+                if len(row.items) > 0:
+                    last_chunk = row.items[-1]
+                    if len(self.layout_rows) > 1 and isinstance(last_chunk, TextLineChunkFTFont):
+                        if (len(last_chunk.text) > 0 and
+                                last_chunk.text[-1] == " " and
+                                row.row_index != (len(self.layout_rows) - 1)):
+                            edit_pos_accumulator += row.letter_count
+                        else:
+                            row.set_cursor_position(cursor_pos - edit_pos_accumulator)
+                            self.cursor_text_row = row
+                            break
+                    elif (len(self.layout_rows) > 1 and
+                          isinstance(last_chunk, LineBreakLayoutRect) and
+                          row.row_index != (len(self.layout_rows) - 1)):
+                        # if the last chunk in a row is a line break and we have more than one row and this isn't the last row
+                        # we want to jump to the start of the next row
+                        edit_pos_accumulator += row.letter_count
+                    else:
+                        row.set_cursor_position(cursor_pos - edit_pos_accumulator)
+                        self.cursor_text_row = row
+                        break
+
             else:
-                letter_acc += row.letter_count
+                edit_pos_accumulator += row.letter_count
+
+        if edit_pos_accumulator == self.letter_count and len(self.layout_rows) > 0:
+            last_row = self.layout_rows[-1]
+            last_row.set_cursor_position(last_row.letter_count)
+
+    def _find_cursor_row_from_click(self, click_pos):
+        found_row = None
+        cursor_click_pos = (click_pos[0], click_pos[1])
+        for count, row in enumerate(self.layout_rows):
+            if count < len(self.layout_rows) - 1:
+                if cursor_click_pos[1] < row.top or cursor_click_pos[1] >= self.layout_rows[count + 1].top:
+                    continue
+            else:
+                if cursor_click_pos[1] < row.top or cursor_click_pos[1] > row.bottom:
+                    continue
+            found_row = row
+
+        if found_row is None and len(self.layout_rows) > 0:
+            if cursor_click_pos[1] > self.layout_rows[-1].bottom:
+                # we are assuming here that the rows are in height order
+                # TODO: check this is always true
+                found_row = self.layout_rows[-1]
+                cursor_click_pos = found_row.midright
+
+            if cursor_click_pos[1] < self.layout_rows[0].top:
+                # we are assuming here that the rows are in height order
+                # TODO: check this is always true
+                found_row = self.layout_rows[0]
+                cursor_click_pos = (cursor_click_pos[0], found_row.centery)
+
+        return found_row, cursor_click_pos
 
     def set_cursor_from_click_pos(self, click_pos):
         """
@@ -562,11 +655,33 @@ def set_cursor_from_click_pos(self, click_pos):
                 self.cursor_text_row.toggle_cursor()
             self.cursor_text_row = None
 
-        for row in self.layout_rows:
-            if click_pos[1] < row.top or click_pos[1] >= row.bottom:
-                continue
-            self.cursor_text_row = row
-            row.set_cursor_from_click_pos(click_pos)
+        found_row, final_click_pos = self._find_cursor_row_from_click(click_pos)
+        self.cursor_text_row = found_row
+        if self.cursor_text_row is not None:
+            self.cursor_text_row.set_cursor_from_click_pos(final_click_pos, len(self.layout_rows))
+
+    def find_cursor_position_from_click_pos(self, click_pos) -> int:
+        """
+        Find an edit text cursor position in the text from a click.
+
+        Here we don't set it, we just find it and return it.
+
+        :param click_pos: This is the pixel position we want to find the nearest cursor spot to.
+        :return: an integer representing the character index position in the text
+        """
+        found_row, final_click_pos = self._find_cursor_row_from_click(click_pos)
+
+        if found_row is not None:
+            cursor_index = 0
+            for row in self.layout_rows:
+                if row == found_row:
+                    cursor_index += found_row.find_cursor_pos_from_click_pos(final_click_pos, len(self.layout_rows))[0]
+                    break
+                else:
+                    cursor_index += row.letter_count
+            return cursor_index
+
+        return 0
 
     def get_cursor_index(self):
         """
@@ -577,7 +692,12 @@ def get_cursor_index(self):
         """
         cursor_index = 0
         if self.cursor_text_row is not None:
-            cursor_index = self.cursor_text_row.get_cursor_index()
+            for row in self.layout_rows:
+                if row == self.cursor_text_row:
+                    cursor_index += self.cursor_text_row.get_cursor_index()
+                    break
+                else:
+                    cursor_index += row.letter_count
         return cursor_index
 
     def toggle_cursor(self):
@@ -589,6 +709,20 @@ def toggle_cursor(self):
         if self.cursor_text_row is not None:
             self.cursor_text_row.toggle_cursor()
 
+    def turn_off_cursor(self):
+        """
+        Makes the edit test cursor invisible.
+        """
+        if self.cursor_text_row is not None:
+            self.cursor_text_row.turn_off_cursor()
+
+    def turn_on_cursor(self):
+        """
+        Makes the edit test cursor visible.
+        """
+        if self.cursor_text_row is not None:
+            self.cursor_text_row.turn_on_cursor()
+
     def set_text_selection(self, start_index, end_index):
         """
         Set a portion of the text layout as 'selected'. This is useful when editing chunks
@@ -627,20 +761,23 @@ def set_text_selection(self, start_index, end_index):
                 self.selected_rows.append(row)
                 rows_to_finalise.add(row)
                 for chunk in row.items:
-                    if chunk == start_chunk:
-                        start_selection = True
-
-                    if start_selection and not end_selection:
-                        if chunk == end_chunk and end_letter_index == 0:
-                            # don't select this
-                            pass
-                        else:
-                            chunk.is_selected = True
-                            chunk.selection_colour = self.selection_colour
-                            self.selected_chunks.append(chunk)
-
-                    if chunk == end_chunk:
-                        end_selection = True
+                    if chunk is not None:
+                        if chunk == start_chunk:
+                            start_selection = True
+
+                        if start_selection and not end_selection:
+                            if chunk == end_chunk and end_letter_index == 0:
+                                # don't select this
+                                pass
+                            else:
+                                chunk.is_selected = True
+                                chunk.selection_colour = self.selection_colour
+                                self.selected_chunks.append(chunk)
+
+                        if chunk == end_chunk:
+                            end_selection = True
+                    else:
+                        print("found None chunk in row: ", row.row_index, " with items: ", row.items)
 
         if self.finalised_surface is not None:
             for row in rows_to_finalise:
@@ -663,14 +800,41 @@ def _find_and_split_chunk(self, index: int, return_rhs: bool = False):
                         break
                     letter_accumulator += chunk.letter_count
                     chunk_in_row_index += 1
-
-        if letter_index != 0:
+                elif isinstance(chunk, LineBreakLayoutRect):
+                    if index_in_row < letter_accumulator + chunk.letter_count:
+                        letter_index = index_in_row - letter_accumulator
+                        found_chunk = chunk
+                        break
+            if found_chunk is None:
+                chunk_in_row_index = 0
+                # couldn't find it on this row so use the first chunk of row below
+                if row_index + 1 < len(self.layout_rows):
+                    chunk_row = self.layout_rows[row_index+1]
+                    row_index = chunk_row.row_index
+                    letter_index = 0
+
+                    for chunk in chunk_row.items:
+                        if isinstance(chunk, TextLineChunkFTFont):
+                            found_chunk = chunk
+                            break
+                        elif isinstance(chunk, LineBreakLayoutRect):
+                            found_chunk = chunk
+                            break
+
+        if found_chunk is not None and found_chunk.can_split():
             # split the chunk
 
             # for the start chunk we want the right hand side of the split
             new_chunk = found_chunk.split_index(letter_index)
             chunk_row.items.insert(chunk_in_row_index + 1, new_chunk)
 
+            first_half = found_chunk
+            second_half = new_chunk
+            if isinstance(first_half, TextLineChunkFTFont):
+                first_half = first_half.text
+            if isinstance(second_half, TextLineChunkFTFont):
+                second_half = second_half.text
+
             if return_rhs:
                 found_chunk = new_chunk
 
@@ -706,21 +870,79 @@ def insert_text(self, text: str, layout_index: int, parser: Optional[HTMLParser]
         """
         current_row, index_in_row = self._find_row_from_text_box_index(layout_index)
         if current_row is not None:
+            if (index_in_row == current_row.letter_count and
+                    current_row.row_index < (len(self.layout_rows) - 1) and
+                    current_row.last_chunk_is_line_break()):
+                current_row = self.layout_rows[current_row.row_index + 1]
+                index_in_row = 0
+
             current_row.insert_text(text, index_in_row, parser)
 
+            row_to_process_from = current_row
+            row_to_process_from_index = current_row.row_index
+            if current_row.row_index > 0:
+                row_to_process_from = self.layout_rows[current_row.row_index - 1]
+                row_to_process_from_index = row_to_process_from.row_index
+
             temp_layout_queue = deque([])
-            for row in reversed(self.layout_rows[current_row.row_index:]):
+            for row in reversed(self.layout_rows[row_to_process_from_index:]):
                 row.rewind_row(temp_layout_queue)
 
-            self.layout_rows = self.layout_rows[:current_row.row_index]
+            self.layout_rows = self.layout_rows[:row_to_process_from_index]
+            self._merge_adjacent_compatible_chunks(temp_layout_queue)
+            self._process_layout_queue(temp_layout_queue, row_to_process_from)
 
+            if self.finalised_surface is not None:
+                for row in self.layout_rows[row_to_process_from_index:]:
+                    row.finalise(self.finalised_surface)
+        else:
+            raise RuntimeError("no rows in text box layout")
+
+    def insert_line_break(self, layout_index: int, parser: Optional[HTMLParser]):
+        """
+        Insert a line break into the text layout at a given point.
+
+        :param layout_index: the character index at which to insert the line break.
+        :param parser: An optional HTML parser for text styling data
+        """
+        found_chunk, index_in_row, row_index = self._find_and_split_chunk(layout_index)
+
+        if len(self.layout_rows) > 0 and found_chunk is not None:
+            current_row = self.layout_rows[row_index]
+            current_row.insert_linebreak_after_chunk(found_chunk, parser)
+
+            temp_layout_queue = deque([])
+            for row in reversed(self.layout_rows[row_index:]):
+                row.rewind_row(temp_layout_queue)
+
+            self.layout_rows = self.layout_rows[:row_index]
+            self._merge_adjacent_compatible_chunks(temp_layout_queue)
             self._process_layout_queue(temp_layout_queue, current_row)
 
             if self.finalised_surface is not None:
                 for row in self.layout_rows[current_row.row_index:]:
                     row.finalise(self.finalised_surface)
-        else:
-            raise RuntimeError("no rows in text box layout")
+        if found_chunk is None:
+            if layout_index == self.letter_count:
+                if len(self.layout_rows) > 0:
+                    last_row = self.layout_rows[-1]
+                    last_chunk = last_row.get_last_text_or_line_break_chunk()
+                    if last_chunk is not None:
+                        last_row.insert_linebreak_after_chunk(last_chunk, parser)
+
+                        temp_layout_queue = deque([])
+                        for row in reversed(self.layout_rows[last_row.row_index:]):
+                            row.rewind_row(temp_layout_queue)
+
+                        self.layout_rows = self.layout_rows[:last_row.row_index]
+                        self._merge_adjacent_compatible_chunks(temp_layout_queue)
+                        self._process_layout_queue(temp_layout_queue, last_row)
+
+                        if self.finalised_surface is not None:
+                            for row in self.layout_rows[last_row.row_index:]:
+                                row.finalise(self.finalised_surface)
+            else:
+                print("couldn't find chunk to split at layout index: ", layout_index)
 
     def delete_selected_text(self):
         """
@@ -745,16 +967,28 @@ def delete_selected_text(self):
         current_row_index = current_row.row_index
         for row in reversed(self.selected_rows):
             row.items = [chunk for chunk in row.items if not chunk.is_selected]
-            row.rewind_row(temp_layout_queue)
             if row.row_index > max_row_index:
                 max_row_index = row.row_index
 
-        for row_index in reversed(range(max_row_index + 1, len(self.layout_rows))):
-            self.layout_rows[row_index].rewind_row(temp_layout_queue)
+        row_to_process_from = current_row
+        row_to_process_from_index = current_row.row_index
+        if current_row.row_index > 0:
+            row_to_process_from = self.layout_rows[current_row.row_index - 1]
+            row_to_process_from_index = row_to_process_from.row_index
 
-        self.layout_rows = self.layout_rows[:current_row_index]
+        temp_layout_queue = deque([])
+        for row in reversed(self.layout_rows[row_to_process_from_index:]):
+            row.rewind_row(temp_layout_queue)
+
+        newly_empty_rows = self.layout_rows[row_to_process_from_index:]
+
+        if self.finalised_surface is not None:
+            for row in newly_empty_rows:
+                row.clear()
 
-        self._process_layout_queue(temp_layout_queue, current_row)
+        self.layout_rows = self.layout_rows[:row_to_process_from_index]
+        self._merge_adjacent_compatible_chunks(temp_layout_queue)
+        self._process_layout_queue(temp_layout_queue, row_to_process_from)
 
         if len(current_row.items) == 0:
             current_row_starting_chunk.text = ''
@@ -765,7 +999,7 @@ def delete_selected_text(self):
             current_row.add_item(current_row_starting_chunk)
 
         if self.finalised_surface is not None:
-            for row in self.layout_rows[current_row_index:]:
+            for row in self.layout_rows[row_to_process_from_index:]:
                 row.finalise(self.finalised_surface)
 
         self.selected_rows = []
@@ -781,24 +1015,50 @@ def delete_at_cursor(self):
             current_row_index = current_row.row_index
             cursor_pos = self.cursor_text_row.cursor_index
             letter_acc = 0
+            deleted_character = False
+            chunk_to_remove = None
             for chunk in self.cursor_text_row.items:
-                if cursor_pos <= letter_acc + (chunk.letter_count - 1):
-                    chunk_letter_pos = cursor_pos - letter_acc
-                    chunk.delete_letter_at_index(chunk_letter_pos)
-                    break
+                if isinstance(chunk, TextLineChunkFTFont):
+                    if cursor_pos <= letter_acc + (chunk.letter_count - 1):
+                        chunk_letter_pos = cursor_pos - letter_acc
+                        chunk.delete_letter_at_index(chunk_letter_pos)
+                        deleted_character = True
+                        break
+                elif isinstance(chunk, LineBreakLayoutRect):
+                    # delete this chunk
+                    if cursor_pos <= letter_acc + (chunk.letter_count - 1):
+                        chunk_to_remove = chunk
+                        deleted_character = True
+                        break
 
                 letter_acc += chunk.letter_count
+            if chunk_to_remove is not None:
+                current_row.items.remove(chunk_to_remove)
+            if not deleted_character:
+                # failed to delete character, must be at end of row - see if we have a row below
+                # if so delete the first character of that row
+                if current_row_index + 1 < len(self.layout_rows):
+                    row_below = self.layout_rows[current_row_index + 1]
+                    for chunk in row_below.items:
+                        if isinstance(chunk, TextLineChunkFTFont):
+                            chunk.delete_letter_at_index(0)
+
+            row_to_process_from = current_row
+            row_to_process_from_index = current_row.row_index
+            if current_row.row_index > 0:
+                row_to_process_from = self.layout_rows[current_row.row_index - 1]
+                row_to_process_from_index = row_to_process_from.row_index
 
             temp_layout_queue = deque([])
-            for row_index in reversed(range(current_row_index, len(self.layout_rows))):
-                self.layout_rows[row_index].rewind_row(temp_layout_queue)
-
-            self.layout_rows = self.layout_rows[:current_row_index]
+            for row in reversed(self.layout_rows[row_to_process_from_index:]):
+                row.rewind_row(temp_layout_queue)
 
-            self._process_layout_queue(temp_layout_queue, current_row)
+            self.layout_rows = self.layout_rows[:row_to_process_from_index]
+            self._merge_adjacent_compatible_chunks(temp_layout_queue)
+            self._process_layout_queue(temp_layout_queue, row_to_process_from)
 
             if self.finalised_surface is not None:
-                for row in self.layout_rows[current_row_index:]:
+                for row in self.layout_rows[row_to_process_from_index:]:
                     row.finalise(self.finalised_surface)
 
     def backspace_at_cursor(self):
@@ -811,24 +1071,47 @@ def backspace_at_cursor(self):
             current_row_index = current_row.row_index
             cursor_pos = self.cursor_text_row.cursor_index
             letter_acc = 0
-            for chunk in self.cursor_text_row.items:
-                if cursor_pos <= letter_acc + chunk.letter_count:
-                    chunk_letter_pos = cursor_pos - letter_acc
-                    chunk.backspace_letter_at_index(chunk_letter_pos)
-                    break
+            if current_row_index > 0 and cursor_pos == 0:
+                # at start of row with rows above
+                # need to delete from end of row above
+                current_row_index = current_row_index - 1
+                current_row = self.layout_rows[current_row_index]
+                cursor_pos = current_row.letter_count
+
+            chunk_to_remove = None
+            for chunk in current_row.items:
+                if isinstance(chunk, TextLineChunkFTFont):
+                    if cursor_pos <= letter_acc + chunk.letter_count:
+                        chunk_letter_pos = cursor_pos - letter_acc
+                        chunk.backspace_letter_at_index(chunk_letter_pos)
+                        break
+                elif isinstance(chunk, LineBreakLayoutRect):
+                    # delete this chunk, line break has an edit pos length of 1
+                    if cursor_pos <= letter_acc + chunk.letter_count:
+                        chunk_to_remove = chunk
+                        break
 
                 letter_acc += chunk.letter_count
-            self.cursor_text_row.set_cursor_position(cursor_pos - 1)
-            temp_layout_queue = deque([])
-            for row_index in reversed(range(current_row_index, len(self.layout_rows))):
-                self.layout_rows[row_index].rewind_row(temp_layout_queue)
+            if chunk_to_remove is not None:
+                current_row.items.remove(chunk_to_remove)
+            current_row.set_cursor_position(cursor_pos - 1)
 
-            self.layout_rows = self.layout_rows[:current_row_index]
+            row_to_process_from = current_row
+            row_to_process_from_index = current_row.row_index
+            if current_row.row_index > 0:
+                row_to_process_from = self.layout_rows[current_row.row_index - 1]
+                row_to_process_from_index = row_to_process_from.row_index
 
-            self._process_layout_queue(temp_layout_queue, current_row)
+            temp_layout_queue = deque([])
+            for row in reversed(self.layout_rows[row_to_process_from_index:]):
+                row.rewind_row(temp_layout_queue)
+
+            self.layout_rows = self.layout_rows[:row_to_process_from_index]
+            self._merge_adjacent_compatible_chunks(temp_layout_queue)
+            self._process_layout_queue(temp_layout_queue, row_to_process_from)
 
             if self.finalised_surface is not None:
-                for row in self.layout_rows[current_row_index:]:
+                for row in self.layout_rows[row_to_process_from_index:]:
                     row.finalise(self.finalised_surface)
 
     def _find_row_from_text_box_index(self, text_box_index: int):
@@ -908,3 +1191,88 @@ def get_cursor_colour(self) -> pygame.Color:
         :return: a pygame.Color object containing the current colour.
         """
         return self.cursor_colour
+
+    @staticmethod
+    def _merge_adjacent_compatible_chunks(chunk_list: deque):
+
+        index = 0
+        while index < len(chunk_list)-1:
+            current_item = chunk_list[index]
+            next_item = chunk_list[index+1]
+            if (isinstance(current_item, TextLineChunkFTFont) and
+                    isinstance(next_item, TextLineChunkFTFont) and
+                    current_item.style_match(next_item)):
+                current_item.add_text(next_item.text)
+                del chunk_list[index+1]
+            else:
+                index += 1
+
+    def fit_layout_rect_height_to_rows(self):
+        if len(self.layout_rows) > 0:
+            self.layout_rect.height = self.layout_rows[-1].bottom - self.layout_rect.top
+
+    def get_cursor_y_pos(self):
+        if self.cursor_text_row is not None:
+            return self.cursor_text_row.top, self.cursor_text_row.bottom
+        else:
+            return 0, 0
+
+    def get_cursor_pos_move_up_one_row(self, last_cursor_horiz_index):
+        """
+        Returns a cursor character position in the row directly above the last horizontal cursor position
+        if possible.
+        """
+        if self.cursor_text_row is not None:
+            cursor_index = 0
+            if self.cursor_text_row is not None:
+                for i in range(0, len(self.layout_rows)):
+                    row = self.layout_rows[i]
+                    if row == self.cursor_text_row:
+                        if (i - 1) >= 0:
+                            row_above = self.layout_rows[i-1]
+                            cursor_index -= row_above.letter_count
+                            row_above_end = row_above.letter_count
+                            if (row_above.row_text_ends_with_a_space() or
+                                    (len(row_above.items) > 0 and
+                                     isinstance(row_above.items[-1], LineBreakLayoutRect))):
+                                row_above_end = row_above.letter_count - 1
+                            cursor_index += min(last_cursor_horiz_index, row_above_end)
+                            break
+                        else:
+                            cursor_index += last_cursor_horiz_index
+                            break
+                    else:
+                        cursor_index += row.letter_count
+            return cursor_index
+        return 0
+
+    def get_cursor_pos_move_down_one_row(self, last_cursor_horiz_index):
+        """
+        Returns a cursor character position in the row directly above the current cursor position
+        if possible.
+        """
+        if self.cursor_text_row is not None:
+            cursor_index = 0
+            if self.cursor_text_row is not None:
+                for i in range(0, len(self.layout_rows)):
+                    row = self.layout_rows[i]
+                    if row == self.cursor_text_row:
+                        if (i + 1) < len(self.layout_rows):
+                            row_below = self.layout_rows[i+1]
+                            cursor_index += row.letter_count
+                            row_below_end = row_below.letter_count
+                            if (row_below.row_text_ends_with_a_space() or
+                                    (len(row_below.items) > 0 and
+                                     isinstance(row_below.items[-1], LineBreakLayoutRect))):
+                                row_below_end = row_below.letter_count - 1
+                            cursor_index += min(last_cursor_horiz_index, row_below_end)
+                            break
+                        else:
+                            cursor_index += last_cursor_horiz_index
+                            break
+                    else:
+                        cursor_index += row.letter_count
+            return min(cursor_index, self.letter_count)
+        return 0
+
+
diff --git a/pygame_gui/core/text/text_box_layout_row.py b/pygame_gui/core/text/text_box_layout_row.py
index 20d29b3d..62ca6f0a 100644
--- a/pygame_gui/core/text/text_box_layout_row.py
+++ b/pygame_gui/core/text/text_box_layout_row.py
@@ -1,11 +1,13 @@
-from typing import Optional, Tuple, List
+from typing import Optional, Tuple, List, Union
 import math
 import pygame
+import pygame.freetype
 
 from pygame_gui.core.text.text_layout_rect import TextLayoutRect
 from pygame_gui.core.text.text_line_chunk import TextLineChunkFTFont
 from pygame_gui.core.text.text_layout_rect import TextFloatPosition
 from pygame_gui.core.text.html_parser import HTMLParser
+from pygame_gui.core.text.line_break_layout_rect import LineBreakLayoutRect
 
 
 class TextBoxLayoutRow(pygame.Rect):
@@ -18,7 +20,7 @@ def __init__(self, row_start_x, row_start_y, row_index, line_spacing, layout):
         self.line_spacing = line_spacing
         self.row_index = row_index
         self.layout = layout
-        self.items: TextLayoutRect = []
+        self.items: List[TextLayoutRect] = []
 
         self.letter_count = 0
 
@@ -32,6 +34,10 @@ def __init__(self, row_start_x, row_start_y, row_index, line_spacing, layout):
         self.edit_right_margin = 2
         self.cursor_index = 0
         self.cursor_draw_width = 0
+        # if we add an empty row and then start adding text, what font do we use?
+        # this one.
+        self.fall_back_font = self.layout.default_font
+        self.surf_row_dirty = False
 
     def __hash__(self):
         return self.row_index
@@ -77,6 +83,8 @@ def add_item(self, item: TextLayoutRect):
                     origin_item.origin_row_y_adjust = self.y_origin - origin_item.y_origin
                     origin_item.top = origin_item.pre_row_rect.top + origin_item.origin_row_y_adjust
 
+            self.fall_back_font = item.font
+
         self.letter_count += item.letter_count
 
     def rewind_row(self, layout_rect_queue):
@@ -221,7 +229,7 @@ def merge_adjacent_compatible_chunks(self):
             else:
                 index += 1
 
-    def finalise(self, surface, current_end_pos: Optional[int] = None,
+    def finalise(self, surface: pygame.Surface, current_end_pos: Optional[int] = None,
                  cumulative_letter_count: Optional[int] = None):
         """
         Finalise this row, turning it into pixels on a pygame surface. Generally done once we are
@@ -234,45 +242,53 @@ def finalise(self, surface, current_end_pos: Optional[int] = None,
                                         Also helps with the 'typewriter' effect.
         """
         self.merge_adjacent_compatible_chunks()
-        for text_chunk in self.items:
-            chunk_view_rect = pygame.Rect(self.layout.layout_rect.left,
-                                          0, self.layout.view_rect.width,
-                                          self.layout.view_rect.height)
-            if isinstance(text_chunk, TextLineChunkFTFont):
-                if current_end_pos is not None and cumulative_letter_count is not None:
-                    if cumulative_letter_count < current_end_pos:
+        if surface == self.layout.finalised_surface and self.layout.layout_rect.height > surface.get_height():
+            self.layout.finalise_to_new()
+        else:
+            if self.surf_row_dirty:
+                self.clear()
+            for text_chunk in self.items:
+                if text_chunk is not None:
+                    chunk_view_rect = pygame.Rect(self.layout.layout_rect.left,
+                                                  0, self.layout.view_rect.width,
+                                                  self.layout.view_rect.height)
+                    if isinstance(text_chunk, TextLineChunkFTFont):
+                        if current_end_pos is not None and cumulative_letter_count is not None:
+                            if cumulative_letter_count < current_end_pos:
+                                text_chunk.finalise(surface, chunk_view_rect, self.y_origin,
+                                                    self.text_chunk_height, self.height,
+                                                    self.layout.x_scroll_offset,
+                                                    current_end_pos - cumulative_letter_count)
+                                cumulative_letter_count += text_chunk.letter_count
+                        else:
+                            text_chunk.finalise(surface, chunk_view_rect, self.y_origin,
+                                                self.text_chunk_height, self.height,
+                                                self.layout.x_scroll_offset)
+                    else:
                         text_chunk.finalise(surface, chunk_view_rect, self.y_origin,
                                             self.text_chunk_height, self.height,
-                                            self.layout.x_scroll_offset,
-                                            current_end_pos - cumulative_letter_count)
-                        cumulative_letter_count += text_chunk.letter_count
+                                            self.layout.x_scroll_offset)
                 else:
-                    text_chunk.finalise(surface, chunk_view_rect, self.y_origin,
-                                        self.text_chunk_height, self.height,
-                                        self.layout.x_scroll_offset)
-            else:
-                text_chunk.finalise(surface, chunk_view_rect, self.y_origin,
-                                    self.text_chunk_height, self.height,
-                                    self.layout.x_scroll_offset)
+                    print(self.items)
 
-        if self.edit_cursor_active:
-            cursor_surface = pygame.surface.Surface(self.cursor_rect.size,
-                                                    flags=pygame.SRCALPHA, depth=32)
+            if self.edit_cursor_active:
+                cursor_surface = pygame.surface.Surface(self.cursor_rect.size,
+                                                        flags=pygame.SRCALPHA, depth=32)
 
-            cursor_surface.fill(self.layout.get_cursor_colour())
-            self.cursor_rect = pygame.Rect((self.x +
-                                            self.cursor_draw_width -
-                                            self.layout.x_scroll_offset),
-                                           self.y,
-                                           2,
-                                           max(0, self.height - 2))
-            surface.blit(cursor_surface, self.cursor_rect, special_flags=pygame.BLEND_PREMULTIPLIED)
+                cursor_surface.fill(self.layout.get_cursor_colour())
+                self.cursor_rect = pygame.Rect((self.x +
+                                                self.cursor_draw_width -
+                                                self.layout.x_scroll_offset),
+                                               self.y,
+                                               2,
+                                               max(0, self.height - 2))
+                surface.blit(cursor_surface, self.cursor_rect, special_flags=pygame.BLEND_PREMULTIPLIED)
 
-        self.target_surface = surface
+            self.target_surface = surface
 
         # pygame.draw.rect(self.target_surface, pygame.Color('#FF0000'), self.layout.layout_rect, 1)
         # pygame.draw.rect(self.target_surface, pygame.Color('#00FF00'), self, 1)
-
+        self.surf_row_dirty = True
         return cumulative_letter_count
 
     def set_default_text_colour(self, colour):
@@ -303,6 +319,7 @@ def toggle_cursor(self):
 
         Generally used to make it flash on and off to catch the attention of the user.
         """
+
         if self.edit_cursor_active:
             self.edit_cursor_active = False
         else:
@@ -312,18 +329,37 @@ def toggle_cursor(self):
             self.clear()
             self.finalise(self.target_surface)
 
+    def turn_off_cursor(self):
+        """
+        Makes the edit test cursor invisible.
+        """
+        self.edit_cursor_active = False
+        if self.target_surface is not None:
+            self.clear()
+            self.finalise(self.target_surface)
+
+    def turn_on_cursor(self):
+        """
+        Makes the edit test cursor visible.
+        """
+        self.edit_cursor_active = True
+        if self.target_surface is not None:
+            self.clear()
+            self.finalise(self.target_surface)
+
     def clear(self):
         """
-        'Clears' the current row from it's target surface by setting the
+        'Clears' the current row from its target surface by setting the
          area taken up by this row to transparent black.
 
          Hopefully the target surface is supposed to be transparent black when empty.
         """
-        if self.target_surface is not None:
+        if self.target_surface is not None and self.surf_row_dirty:
             slightly_wider_rect = pygame.Rect(self.x, self.y,
                                               self.layout.view_rect.width,
                                               self.height)
             self.target_surface.fill(pygame.Color('#00000000'), slightly_wider_rect)
+            self.surf_row_dirty = False
 
     def _setup_offset_position_from_edit_cursor(self):
         if self.cursor_draw_width > (self.layout.x_scroll_offset +
@@ -335,11 +371,24 @@ def _setup_offset_position_from_edit_cursor(self):
             self.layout.x_scroll_offset = max(0, self.cursor_draw_width -
                                               self.edit_cursor_left_margin)
 
-    def set_cursor_from_click_pos(self, click_pos: Tuple[int, int]):
+    def set_cursor_from_click_pos(self, click_pos: Tuple[int, int], num_rows: int):
         """
         Set the current edit cursor position from a pixel position - usually
         originating from a mouse click.
 
+        :param num_rows:
+        :param click_pos: The pixel position to use.
+        """
+        self.cursor_index, self.cursor_draw_width = self.find_cursor_pos_from_click_pos(click_pos, num_rows)
+
+        self._setup_offset_position_from_edit_cursor()
+
+    def find_cursor_pos_from_click_pos(self, click_pos: Tuple[int, int], num_rows: int):
+        """
+        Find an edit cursor position from a pixel position - usually
+        originating from a mouse click.
+
+        :param num_rows:
         :param click_pos: The pixel position to use.
         """
         letter_acc = 0
@@ -349,7 +398,13 @@ def set_cursor_from_click_pos(self, click_pos: Tuple[int, int]):
         for chunk in self.items:
             if isinstance(chunk, TextLineChunkFTFont):
                 if not found_chunk:
-                    if chunk.collidepoint(scrolled_click_pos):
+                    # we only care about the X position at this point.
+                    if chunk == self.items[0] and scrolled_click_pos[0] < chunk.left:
+                        letter_index = 0
+                        cursor_draw_width = 0
+                        letter_acc += letter_index
+                        found_chunk = True
+                    elif chunk.collidepoint((scrolled_click_pos[0], chunk.centery)):
                         letter_index = chunk.x_pos_to_letter_index(scrolled_click_pos[0])
                         cursor_draw_width += sum([char_metric[4]
                                                   for char_metric in
@@ -362,16 +417,28 @@ def set_cursor_from_click_pos(self, click_pos: Tuple[int, int]):
                                                   for char_metric in
                                                   chunk.font.get_metrics(chunk.text) if char_metric])
                         letter_acc += chunk.letter_count
-        if not found_chunk:
-            # not inside chunk so move to start of line
-            # if we are on left, should be at end of line already
-            if scrolled_click_pos[0] < self.left:
-                cursor_draw_width = 0
-                letter_acc = 0
-        self.cursor_draw_width = cursor_draw_width
-        self.cursor_index = min(self.letter_count, max(0, letter_acc))
+        if (not found_chunk and scrolled_click_pos[0] >= self.right) or (letter_acc == self.letter_count):
+            # if we have more than two rows check if we are on right of whole row and if row has space at the end.
+            # If so stick the edit cursor before the space because this is how it works.
+            if num_rows > 1 and self.row_text_ends_with_a_space():
+                letter_acc -= 1
+                last_chunk = self.get_last_text_chunk()
+                if last_chunk is not None:
+                    char_metric = last_chunk.font.get_metrics(" ")[0]
+                    if char_metric:
+                        cursor_draw_width -= char_metric[4]
+
+        cursor_index = min(self.letter_count, max(0, letter_acc))
+        return cursor_index, cursor_draw_width
+
+    def get_last_text_chunk(self):
+        last_item = None
+        for item in reversed(self.items):
+            if isinstance(item, TextLineChunkFTFont):
+                last_item = item
+                break
 
-        self._setup_offset_position_from_edit_cursor()
+        return last_item
 
     def set_cursor_position(self, cursor_pos):
         """
@@ -383,18 +450,21 @@ def set_cursor_position(self, cursor_pos):
         letter_acc = 0
         cursor_draw_width = 0
         for chunk in self.items:
-            if cursor_pos <= letter_acc + chunk.letter_count:
-                chunk_letter_pos = cursor_pos - letter_acc
-                cursor_draw_width += sum([char_metric[4]
-                                          for char_metric
-                                          in chunk.font.get_metrics(chunk.text[:chunk_letter_pos]) if char_metric])
+            if isinstance(chunk, TextLineChunkFTFont):
+                if cursor_pos <= letter_acc + chunk.letter_count:
+                    chunk_letter_pos = cursor_pos - letter_acc
+                    cursor_draw_width += sum([char_metric[4]
+                                              for char_metric
+                                              in chunk.font.get_metrics(chunk.text[:chunk_letter_pos]) if char_metric])
 
-                break
+                    break
 
-            letter_acc += chunk.letter_count
-            cursor_draw_width += sum([char_metric[4]
-                                      for char_metric in
-                                      chunk.font.get_metrics(chunk.text) if char_metric])
+                letter_acc += chunk.letter_count
+                cursor_draw_width += sum([char_metric[4]
+                                          for char_metric in
+                                          chunk.font.get_metrics(chunk.text) if char_metric])
+            elif isinstance(chunk, LineBreakLayoutRect):
+                pass
 
         self.cursor_draw_width = cursor_draw_width
 
@@ -420,10 +490,11 @@ def insert_text(self, text: str, letter_row_index: int,
         letter_acc = 0
         if len(self.items) > 0:
             for chunk in self.items:
-                if letter_row_index <= letter_acc + chunk.letter_count:
-                    chunk_index = letter_row_index - letter_acc
-                    chunk.insert_text(text, chunk_index)
-                    break
+                if isinstance(chunk, TextLineChunkFTFont):
+                    if letter_row_index <= letter_acc + chunk.letter_count:
+                        chunk_index = letter_row_index - letter_acc
+                        chunk.insert_text(text, chunk_index)
+                        break
 
                 letter_acc += chunk.letter_count
         elif parser is not None:
@@ -432,3 +503,44 @@ def insert_text(self, text: str, letter_row_index: int,
         else:
             raise AttributeError("Trying to insert into empty text row with no Parser"
                                  " for style data - fix this later?")
+
+    def insert_linebreak_after_chunk(self, chunk_to_insert_after: Union[TextLineChunkFTFont, LineBreakLayoutRect], parser: HTMLParser):
+        if len(self.items) > 0:
+            chunk_insert_index = 0
+            for chunk_index, chunk in enumerate(self.items):
+                if chunk == chunk_to_insert_after:
+                    chunk_insert_index = chunk_index + 1
+                    break
+            current_font: pygame.freetype.Font = chunk_to_insert_after.font
+            current_font_size = current_font.size
+            dimensions = (current_font.get_rect(' ').width,
+                          int(round(current_font_size *
+                                    self.line_spacing)))
+            self.items.insert(chunk_insert_index, LineBreakLayoutRect(dimensions, current_font))
+            empty_text_chunk = parser.create_styled_text_chunk('')
+            self.items.insert(chunk_insert_index + 1, empty_text_chunk)
+
+    def row_text_ends_with_a_space(self):
+        for item in reversed(self.items):
+            if isinstance(item, TextLineChunkFTFont):
+                if len(item.text) > 0 and item.text[-1] == " ":
+                    return True
+        return False
+
+    def get_last_text_chunk(self):
+        for item in reversed(self.items):
+            if isinstance(item, TextLineChunkFTFont):
+                return item
+        return None
+
+    def get_last_text_or_line_break_chunk(self):
+        for item in reversed(self.items):
+            if isinstance(item, TextLineChunkFTFont) or isinstance(item, LineBreakLayoutRect):
+                return item
+        return None
+
+    def last_chunk_is_line_break(self):
+        if len(self.items) > 0:
+            if isinstance(self.items[-1], LineBreakLayoutRect):
+                return True
+        return False
diff --git a/pygame_gui/core/text/text_layout_rect.py b/pygame_gui/core/text/text_layout_rect.py
index 6009a6c1..1b5e161e 100644
--- a/pygame_gui/core/text/text_layout_rect.py
+++ b/pygame_gui/core/text/text_layout_rect.py
@@ -106,7 +106,8 @@ def float_pos(self) -> TextFloatPosition:
     def split(self,
               requested_x: int,
               line_width: int,
-              row_start_x: int) -> Union['TextLayoutRect', None]:  # noqa
+              row_start_x: int,
+              allow_split_dashes: bool = True) -> Union['TextLayoutRect', None]:  # noqa
         """
         Try to perform a split operation on this rectangle. Often rectangles will be split at the
         nearest point that is still less than the request (i.e. to the left of the request in
@@ -115,6 +116,8 @@ def split(self,
         :param requested_x: the requested place to split this rectangle along it's width.
         :param line_width: the width of the current line.
         :param row_start_x: the x start position of the row.
+        :param allow_split_dashes: whether we allow text to be split with dashes either side.
+                                   allowing this makes direct text editing more annoying.
 
         """
         if line_width < self.smallest_split_size:
diff --git a/pygame_gui/core/text/text_line_chunk.py b/pygame_gui/core/text/text_line_chunk.py
index fd4b294e..69b6cdf2 100644
--- a/pygame_gui/core/text/text_line_chunk.py
+++ b/pygame_gui/core/text/text_line_chunk.py
@@ -95,6 +95,9 @@ def __init__(self, text: str,
         self.effects_offset_pos = (0, 0)
         self.transform_effect_rect = pygame.Rect(self.topleft, self.size)
 
+    def __repr__(self):
+        return "< '" + self.text + "' " + super().__repr__() + " >"
+
     def _handle_dimensions(self, font, max_dimensions, text):
         if len(text) == 0:
             text_rect = font.get_rect('A')
@@ -373,7 +376,8 @@ def _apply_shadow_effect(self, surface, text_rect, text_str, text_surface, origi
     def split(self,
               requested_x: int,
               line_width: int,
-              row_start_x: int) -> Union['TextLayoutRect', None]:
+              row_start_x: int,
+              allow_split_dashes: bool = True) -> Union['TextLayoutRect', None]:
         """
         Try to perform a split operation on this chunk at the requested pixel position.
 
@@ -381,9 +385,11 @@ def split(self,
         the request (i.e. to the left of the request in the common left-to-right
         text layout case) .
 
-        :param requested_x: the requested place to split this rectangle along it's width.
+        :param requested_x: the requested place to split this rectangle along its width.
         :param line_width: the width of the current line.
         :param row_start_x: the x start position of the row.
+        :param allow_split_dashes: whether we allow text to be split with dashes either side.
+                                   allowing this makes direct text editing more annoying.
         """
         # starting heuristic: find the percentage through the chunk width of this split request
         percentage_split = 0
@@ -405,13 +411,18 @@ def split(self,
             # we have a chunk with no breaks (one long word usually) longer than a line
             # split it at the word
             optimum_split_point = max(1, int(percentage_split * len(self.text)) - 1)
-            if optimum_split_point != 1:
-                # have to be at least wide enough to fit in a dash and another character
-                left_side = self.text[:optimum_split_point] + '-'
-                right_side = '-' + self.text[optimum_split_point:]
-                split_text_ok = True
+            if allow_split_dashes:
+                if optimum_split_point != 1:
+                    # have to be at least wide enough to fit in a dash and another character
+                    left_side = self.text[:optimum_split_point] + '-'
+                    right_side = '-' + self.text[optimum_split_point:]
+                    split_text_ok = True
+                else:
+                    raise ValueError('Line width is too narrow')
             else:
-                raise ValueError('Line width is too narrow')
+                left_side = self.text[:optimum_split_point]
+                right_side = self.text[optimum_split_point:]
+                split_text_ok = True
 
         if split_text_ok:
             # update the data for this chunk
@@ -473,7 +484,7 @@ def split_index(self, index):
 
         :param index: the requested index at which to split this rectangle along it's width.
         """
-        if 0 < index < len(self.text):
+        if 0 <= index < len(self.text):
             left_side = self.text[:index]
             right_side = self.text[index:]
 
@@ -486,7 +497,12 @@ def split_index(self, index):
 
             return self._split_at(right_side, self.topright, self.target_surface,
                                   self.target_surface_area, self.should_centre_from_baseline)
+        elif index == 0 and len(self.text) == 0:
+            # special case
+            return self._split_at("", self.topright, self.target_surface,
+                                  self.target_surface_area, self.should_centre_from_baseline)
         else:
+            print("index is bad at: ", index, "len(self.text): ", len(self.text))
             return None
 
     def _split_at(self, right_side, split_pos, target_surface,
@@ -605,7 +621,7 @@ def backspace_letter_at_index(self, index):
     def x_pos_to_letter_index(self, x_pos: int):
         """
         Convert a horizontal, or 'x' pixel position into a letter/character index in this
-        text chunk. Commonly used fro converting mouse clicks into letter positions for
+        text chunk. Commonly used for converting mouse clicks into letter positions for
         positioning the text editing cursor/carat.
         """
 
@@ -635,7 +651,6 @@ def x_pos_to_letter_index(self, x_pos: int):
                 check_dir *= -1
                 changed_dir += 1
                 step = 1
-
         return best_index
 
     def redraw(self):
diff --git a/pygame_gui/core/ui_element.py b/pygame_gui/core/ui_element.py
index 88177b98..8b4a5f48 100644
--- a/pygame_gui/core/ui_element.py
+++ b/pygame_gui/core/ui_element.py
@@ -723,7 +723,6 @@ def check_hover(self, time_delta: float, hovered_higher_element: bool) -> bool:
                     self.hovered = True
                     self.on_hovered()
 
-                hovered_higher_element = True
                 self.while_hovering(time_delta, mouse_pos)
 
             else:
diff --git a/pygame_gui/core/ui_window_stack.py b/pygame_gui/core/ui_window_stack.py
index a3f0410b..8b3d5364 100644
--- a/pygame_gui/core/ui_window_stack.py
+++ b/pygame_gui/core/ui_window_stack.py
@@ -33,9 +33,10 @@ def add_new_window(self, window: IWindowInterface):
         :param window: The window to add.
 
         """
-        new_layer = (self.stack[-1].get_top_layer()
+        new_layer = (self.stack[-1].get_top_layer() + 1
                      if len(self.stack) > 0
-                     else self.root_container.get_top_layer())
+                     else self.root_container.get_top_layer() + 1)
+
         window.change_layer(new_layer)
         self.stack.append(window)
         window.on_moved_to_front()
diff --git a/pygame_gui/elements/ui_drop_down_menu.py b/pygame_gui/elements/ui_drop_down_menu.py
index 12ce795d..889e8469 100644
--- a/pygame_gui/elements/ui_drop_down_menu.py
+++ b/pygame_gui/elements/ui_drop_down_menu.py
@@ -673,6 +673,7 @@ def __init__(self,
                                object_id=object_id,
                                element_id='drop_down_menu')
 
+        self.__layer_thickness_including_expansion = 4
         self.options_list = options_list
         self.selected_option = starting_option
         self.open_button_width = 20
@@ -723,6 +724,15 @@ def __init__(self,
         self.current_state = self.menu_states['closed']
         self.current_state.start(should_rebuild=True)
 
+    @property
+    def layer_thickness(self):
+        return self.__layer_thickness_including_expansion
+
+    @layer_thickness.setter
+    def layer_thickness(self, value):
+        # ignore passed in value and hardcode to 4
+        self.__layer_thickness_including_expansion = 4
+
     def add_options(self, new_options: List[str]) -> None:
         """
         Add new options to the drop down. Will close the drop down if it is currently open.
diff --git a/pygame_gui/elements/ui_text_box.py b/pygame_gui/elements/ui_text_box.py
index b1605c5d..471f6550 100644
--- a/pygame_gui/elements/ui_text_box.py
+++ b/pygame_gui/elements/ui_text_box.py
@@ -1,5 +1,6 @@
 import warnings
 import math
+import html
 
 from typing import Union, Tuple, Dict, Optional, Any
 
@@ -85,7 +86,10 @@ def __init__(self,
                  visible: int = 1,
                  *,
                  pre_parsing_enabled: bool = True,
-                 text_kwargs: Optional[Dict[str, str]] = None):
+                 text_kwargs: Optional[Dict[str, str]] = None,
+                 allow_split_dashes: bool = True,
+                 plain_text_display_only: bool = False,
+                 should_html_unescape_input_text: bool = False):
 
         super().__init__(relative_rect, manager, container,
                          starting_height=layer_starting_height,
@@ -98,13 +102,18 @@ def __init__(self,
                                parent_element=parent_element,
                                object_id=object_id,
                                element_id='text_box')
-
-        self.html_text = html_text
+        self.should_html_unescape_input_text = should_html_unescape_input_text
+        if self.should_html_unescape_input_text:
+            self.html_text = html.unescape(html_text)
+        else:
+            self.html_text = html_text
         self.appended_text = ""
         self.text_kwargs = {}
         if text_kwargs is not None:
             self.text_kwargs = text_kwargs
         self.font_dict = self.ui_theme.get_font_dictionary()
+        self.allow_split_dashes = allow_split_dashes
+        self.plain_text_display_only = plain_text_display_only
 
         self._pre_parsing_enabled = pre_parsing_enabled
 
@@ -317,6 +326,13 @@ def rebuild(self):
         self.should_trigger_full_rebuild = False
         self.full_rebuild_countdown = self.time_until_full_rebuild_after_changing_size
 
+    def get_text_layout_top_left(self):
+        return (self.rect.left + self.padding[0] + self.border_width +
+                self.shadow_width + self.rounded_corner_offset,
+                self.rect.top + self.padding[1] + self.border_width +
+                self.shadow_width + self.rounded_corner_offset
+                )
+
     def _align_all_text_rows(self):
         """
         Aligns the text drawing position correctly according to our theming options.
@@ -525,15 +541,29 @@ def parse_html_into_style_data(self):
         Parses HTML styled string text into a format more useful for styling pygame.freetype
         rendered text.
         """
-
-        self.parser.feed(self._pre_parse_text(translate(self.html_text, **self.text_kwargs) + self.appended_text))
-
+        feed_input = self.html_text
+        if self.plain_text_display_only:
+            feed_input = html.escape(feed_input)  # might have to add true to second param here for quotes
+        feed_input = self._pre_parse_text(translate(feed_input, **self.text_kwargs) + self.appended_text)
+        self.parser.feed(feed_input)
+
+        default_font = self.ui_theme.get_font_dictionary().find_font(
+            font_name=self.parser.default_style['font_name'],
+            font_size=self.parser.default_style['font_size'],
+            bold=self.parser.default_style['bold'],
+            italic=self.parser.default_style['italic'])
+        default_font_data = {"font": default_font,
+                             "font_colour": self.parser.default_style['font_colour'],
+                             "bg_colour": self.parser.default_style['bg_colour']
+                             }
         self.text_box_layout = TextBoxLayout(self.parser.layout_rect_queue,
                                              pygame.Rect((0, 0), (self.text_wrap_rect[2],
                                                                   self.text_wrap_rect[3])),
                                              pygame.Rect((0, 0), (self.text_wrap_rect[2],
                                                                   self.text_wrap_rect[3])),
-                                             line_spacing=1.25)
+                                             line_spacing=1.25,
+                                             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:
             self.text_box_layout.view_rect.height = self.text_box_layout.layout_rect.height
@@ -549,36 +579,45 @@ def redraw_from_text_block(self):
         """
         if self.rect.width <= 0 or self.rect.height <= 0:
             return
-        if self.scroll_bar is not None:
-            height_adjustment = int(self.scroll_bar.start_percentage *
-                                    self.text_box_layout.layout_rect.height)
-            percentage_visible = (self.text_wrap_rect[3] /
-                                  self.text_box_layout.layout_rect.height)
-            self.scroll_bar.set_visible_percentage(percentage_visible)
+        if (self.scroll_bar is None and
+                (self.text_box_layout.layout_rect.height > self.text_wrap_rect[3])):
+            self.rebuild()
         else:
-            height_adjustment = 0
-        drawable_area_size = (max(1, (self.rect[2] -
-                                      (self.padding[0] * 2) -
-                                      (self.border_width * 2) -
-                                      (self.shadow_width * 2) -
-                                      (2 * self.rounded_corner_offset))),
-                              max(1, (self.rect[3] -
-                                      (self.padding[1] * 2) -
-                                      (self.border_width * 2) -
-                                      (self.shadow_width * 2) -
-                                      (2 * self.rounded_corner_offset))))
-        drawable_area = pygame.Rect((0, height_adjustment),
-                                    drawable_area_size)
-        new_image = pygame.surface.Surface(self.rect.size, flags=pygame.SRCALPHA, depth=32)
-        new_image.fill(pygame.Color(0, 0, 0, 0))
-        basic_blit(new_image, self.background_surf, (0, 0))
-        basic_blit(new_image, self.text_box_layout.finalised_surface,
-                   (self.padding[0] + self.border_width +
-                    self.shadow_width + self.rounded_corner_offset,
-                    self.padding[1] + self.border_width +
-                    self.shadow_width + self.rounded_corner_offset),
-                   drawable_area)
-        self._set_image(new_image)
+            if self.scroll_bar is not None:
+                height_adjustment = int(self.scroll_bar.start_percentage *
+                                        self.text_box_layout.layout_rect.height)
+                percentage_visible = (self.text_wrap_rect[3] /
+                                      self.text_box_layout.layout_rect.height)
+                if percentage_visible >= 1.0:
+                    self.scroll_bar.kill()
+                    self.scroll_bar = None
+                    height_adjustment = 0
+                else:
+                    self.scroll_bar.set_visible_percentage(percentage_visible)
+            else:
+                height_adjustment = 0
+            drawable_area_size = (max(1, (self.rect[2] -
+                                          (self.padding[0] * 2) -
+                                          (self.border_width * 2) -
+                                          (self.shadow_width * 2) -
+                                          (2 * self.rounded_corner_offset))),
+                                  max(1, (self.rect[3] -
+                                          (self.padding[1] * 2) -
+                                          (self.border_width * 2) -
+                                          (self.shadow_width * 2) -
+                                          (2 * self.rounded_corner_offset))))
+            drawable_area = pygame.Rect((0, height_adjustment),
+                                        drawable_area_size)
+            new_image = pygame.surface.Surface(self.rect.size, flags=pygame.SRCALPHA, depth=32)
+            new_image.fill(pygame.Color(0, 0, 0, 0))
+            basic_blit(new_image, self.background_surf, (0, 0))
+            basic_blit(new_image, self.text_box_layout.finalised_surface,
+                       (self.padding[0] + self.border_width +
+                        self.shadow_width + self.rounded_corner_offset,
+                        self.padding[1] + self.border_width +
+                        self.shadow_width + self.rounded_corner_offset),
+                       drawable_area)
+            self._set_image(new_image)
 
     def redraw_from_chunks(self):
         """
@@ -864,8 +903,18 @@ def append_html_text(self, new_html_str: str):
 
         :param new_html_str: The, potentially HTML tag, containing string of text to append.
         """
-        self.appended_text += new_html_str
-        self.parser.feed(self._pre_parse_text(new_html_str))
+        if self.should_html_unescape_input_text:
+            self.appended_text += html.unescape(new_html_str)
+        else:
+            self.appended_text += new_html_str
+        feed_input = self.appended_text
+        if self.plain_text_display_only:
+            # if we are supporting only plain text rendering then we turn html input into text at this point
+            feed_input = html.escape(feed_input)  # might have to add true to second param here for quotes
+        # this converts plain text special characters to html just for the parser
+        feed_input = self._pre_parse_text(feed_input)
+
+        self.parser.feed(feed_input)
         self.text_box_layout.append_layout_rects(self.parser.layout_rect_queue)
         self.parser.empty_layout_queue()
 
@@ -1065,7 +1114,10 @@ def get_object_id(self) -> str:
         return self.most_specific_combined_id
 
     def set_text(self, html_text: str, *, text_kwargs: Optional[Dict[str, str]] = None):
-        self.html_text = html_text
+        if self.should_html_unescape_input_text:
+            self.html_text = html.unescape(html_text)
+        else:
+            self.html_text = html_text
         if text_kwargs is not None:
             self.text_kwargs = text_kwargs
         else:
diff --git a/pygame_gui/elements/ui_text_entry_box.py b/pygame_gui/elements/ui_text_entry_box.py
index 2eff9a12..5471a995 100644
--- a/pygame_gui/elements/ui_text_entry_box.py
+++ b/pygame_gui/elements/ui_text_entry_box.py
@@ -1,6 +1,14 @@
-from typing import Union, Tuple, Dict, Optional, Any
+import re
+from typing import Union, Tuple, Dict, Optional
 
-from pygame import Rect
+from pygame_gui.core.utility import clipboard_paste, clipboard_copy
+
+from pygame import Rect, MOUSEBUTTONDOWN, MOUSEBUTTONUP, BUTTON_LEFT, KEYDOWN
+from pygame import KMOD_META, KMOD_CTRL, K_a, K_x, K_c, K_v
+from pygame import K_LEFT, K_RIGHT, K_UP, K_DOWN, K_HOME, K_END, K_BACKSPACE, K_DELETE, K_RETURN
+from pygame import key
+from pygame.event import Event, post
+from pygame_gui._constants import UI_TEXT_ENTRY_CHANGED
 from pygame_gui.core import ObjectID
 from pygame_gui.core.ui_element import UIElement
 from pygame_gui.core.interfaces import IContainerLikeInterface, IUIManagerInterface
@@ -10,7 +18,7 @@
 class UITextEntryBox(UITextBox):
     def __init__(self,
                  relative_rect: Union[Rect, Tuple[int, int, int, int]],
-                 initial_text: str,
+                 initial_text: str = "",
                  manager: Optional[IUIManagerInterface] = None,
                  container: Optional[IContainerLikeInterface] = None,
                  parent_element: Optional[UIElement] = None,
@@ -24,4 +32,545 @@ def __init__(self,
                          parent_element=parent_element,
                          object_id=object_id,
                          anchors=anchors,
-                         visible=visible)
+                         visible=visible,
+                         allow_split_dashes=False,
+                         plain_text_display_only=True,
+                         should_html_unescape_input_text=True)
+
+        # input timings - I expect nobody really wants to mess with these that much
+        # ideally we could populate from the os settings but that sounds like a headache
+        self.key_repeat = 0.5
+        self.cursor_blink_delay_after_moving_acc = 0.0
+        self.cursor_blink_delay_after_moving = 1.0
+        self.blink_cursor_time_acc = 0.0
+        self.blink_cursor_time = 0.4
+
+        self.double_click_timer = self.ui_manager.get_double_click_time() + 1.0
+
+        self.edit_position = 0
+        self._select_range = [0, 0]
+        self.selection_in_progress = False
+
+        self.cursor_on = False
+        self.cursor_has_moved_recently = False
+        self.vertical_cursor_movement = False
+        self.last_horiz_cursor_index = 0
+
+    def get_text(self) -> str:
+        """
+        Gets the text in the entry box element.
+
+        :return: A string.
+
+        """
+        return self.html_text
+
+    def set_text(self, html_text: str, *, text_kwargs: Optional[Dict[str, str]] = None):
+        super().set_text(html_text, text_kwargs=text_kwargs)
+        self.edit_position = len(self.html_text)
+        self.cursor_has_moved_recently = True
+
+    @property
+    def select_range(self):
+        """
+        The selected range for this text. A tuple containing the start
+        and end indexes of the current selection.
+
+        Made into a property to keep it synchronised with the underlying drawable shape's
+        representation.
+        """
+        return self._select_range
+
+    @select_range.setter
+    def select_range(self, value):
+        self._select_range = value
+        start_select = min(self._select_range[0], self._select_range[1])
+        end_select = max(self._select_range[0], self._select_range[1])
+
+        self.text_box_layout.set_text_selection(start_select, end_select)
+        self.redraw_from_text_block()
+
+    def unfocus(self):
+        """
+        Called when this element is no longer the current focus.
+        """
+        super().unfocus()
+        key.set_repeat(0)
+        self.select_range = [0, 0]
+        self.edit_position = 0
+        self.cursor_on = False
+        self.text_box_layout.turn_off_cursor()
+        self.redraw_from_text_block()
+
+    def focus(self):
+        """
+        Called when we 'select focus' on this element. In this case it sets up the keyboard to
+        repeat held key presses, useful for natural feeling keyboard input.
+        """
+        super().focus()
+        key.set_repeat(500, 25)
+
+    def update(self, time_delta: float):
+        """
+        Called every update loop of our UI Manager. Largely handles text drag selection and
+        making sure our edit cursor blinks on and off.
+
+        :param time_delta: The time in seconds between this update method call and the previous one.
+
+        """
+        super().update(time_delta)
+
+        if not self.alive():
+            return
+        scaled_mouse_pos = self.ui_manager.get_mouse_position()
+        if self.hovered:
+            if (self.scroll_bar is None or
+                    (self.scroll_bar is not None and
+                     not self.scroll_bar.hover_point(scaled_mouse_pos[0], scaled_mouse_pos[1]))):
+                self.ui_manager.set_text_input_hovered(True)
+
+        if self.double_click_timer < self.ui_manager.get_double_click_time():
+            self.double_click_timer += time_delta
+
+        if self.selection_in_progress:
+            text_layout_space_pos = self._calculate_text_space_pos(scaled_mouse_pos)
+            select_end_pos = self.text_box_layout.find_cursor_position_from_click_pos(text_layout_space_pos)
+            new_range = [self.select_range[0], select_end_pos]
+            if new_range[0] != self.select_range[0] or new_range[1] != self.select_range[1]:
+                self.select_range = [new_range[0], new_range[1]]
+
+                self.edit_position = self.select_range[1]
+                self.cursor_has_moved_recently = True
+
+        if self.cursor_has_moved_recently:
+            self.cursor_has_moved_recently = False
+            self.cursor_blink_delay_after_moving_acc = 0.0
+            self.cursor_on = True
+            self.text_box_layout.set_cursor_position(self.edit_position)
+            if not self.vertical_cursor_movement:
+                if self.text_box_layout.cursor_text_row is not None:
+                    self.last_horiz_cursor_index = self.text_box_layout.cursor_text_row.get_cursor_index()
+            self.vertical_cursor_movement = False
+            self.text_box_layout.turn_on_cursor()
+            if self.scroll_bar is not None:
+                cursor_y_pos_top, cursor_y_pos_bottom = self.text_box_layout.get_cursor_y_pos()
+                current_height_adjustment = int(self.scroll_bar.start_percentage *
+                                                self.text_box_layout.layout_rect.height)
+                visible_bottom = current_height_adjustment + self.text_box_layout.view_rect.height
+                # handle cursor moving above current scroll position
+                if cursor_y_pos_top < current_height_adjustment:
+                    new_start_percentage = cursor_y_pos_top / self.text_box_layout.layout_rect.height
+                    self.scroll_bar.set_scroll_from_start_percentage(new_start_percentage)
+                # handle cursor moving below visible area
+                if cursor_y_pos_bottom > visible_bottom:
+                    new_top = cursor_y_pos_bottom - self.text_box_layout.view_rect.height
+                    new_start_percentage = new_top / self.text_box_layout.layout_rect.height
+                    self.scroll_bar.set_scroll_from_start_percentage(new_start_percentage)
+
+            self.redraw_from_text_block()
+
+        if self.cursor_blink_delay_after_moving_acc > self.cursor_blink_delay_after_moving:
+            if self.blink_cursor_time_acc >= self.blink_cursor_time:
+                self.blink_cursor_time_acc = 0.0
+                if self.cursor_on:
+                    self.cursor_on = False
+                    self.text_box_layout.toggle_cursor()
+                    self.redraw_from_text_block()
+                elif self.is_focused:
+                    self.cursor_on = True
+                    self.text_box_layout.toggle_cursor()
+                    self.redraw_from_text_block()
+            else:
+                self.blink_cursor_time_acc += time_delta
+        else:
+            self.cursor_blink_delay_after_moving_acc += time_delta
+
+    def _calculate_text_space_pos(self, scaled_mouse_pos):
+        text_layout_top_left = self.get_text_layout_top_left()
+        height_adjustment = 0
+        if self.scroll_bar is not None:
+            height_adjustment = (self.scroll_bar.start_percentage * self.text_box_layout.layout_rect.height)
+        text_layout_space_pos = (scaled_mouse_pos[0] - text_layout_top_left[0],
+                                 scaled_mouse_pos[1] - text_layout_top_left[1] + height_adjustment)
+        return text_layout_space_pos
+
+    def process_event(self, event: Event) -> bool:
+        """
+        Allows the text entry box to react to input events, which is it's primary function.
+        The entry element reacts to various types of mouse clicks (double click selecting words,
+        drag select), keyboard combos (CTRL+C, CTRL+V, CTRL+X, CTRL+A), individual editing keys
+        (Backspace, Delete, Left & Right arrows) and other keys for inputting letters, symbols
+        and numbers.
+
+        :param event: The current event to consider reacting to.
+
+        :return: Returns True if we've done something with the input event.
+
+        """
+        consumed_event = False
+        initial_text_state = self.html_text
+
+        if self._process_mouse_button_event(event):
+            consumed_event = True
+        if self.is_enabled and self.is_focused and event.type == KEYDOWN:
+            if self._process_keyboard_shortcut_event(event):
+                consumed_event = True
+            elif self._process_action_key_event(event):
+                consumed_event = True
+            elif self._process_text_entry_key(event):
+                consumed_event = True
+
+        if self.html_text != initial_text_state:
+            # new event
+            event_data = {'text': self.html_text,
+                          'ui_element': self,
+                          'ui_object_id': self.most_specific_combined_id}
+            post(Event(UI_TEXT_ENTRY_CHANGED, event_data))
+
+        return consumed_event
+
+    def _process_action_key_event(self, event: Event) -> bool:
+        """
+        Check if event is one of the keys that triggers an action like deleting, or moving
+        the edit position.
+
+        :param event: The event to check.
+
+        :return: True if event is consumed.
+
+        """
+        consumed_event = False
+
+        if event.key == K_BACKSPACE:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                self.text_box_layout.delete_selected_text()
+                low_end = min(self.select_range[0], self.select_range[1])
+                high_end = max(self.select_range[0], self.select_range[1])
+                self.html_text = self.html_text[:low_end] + self.html_text[high_end:]
+                self.edit_position = low_end
+                self.select_range = [0, 0]
+                self.cursor_has_moved_recently = True
+
+                self.text_box_layout.set_cursor_position(self.edit_position)
+                self.redraw_from_text_block()
+            elif self.edit_position > 0:
+                self.html_text = self.html_text[:self.edit_position - 1] + self.html_text[self.edit_position:]
+                self.edit_position -= 1
+                self.cursor_has_moved_recently = True
+
+                self.text_box_layout.backspace_at_cursor()
+                self.text_box_layout.set_cursor_position(self.edit_position)
+                self.redraw_from_text_block()
+
+            consumed_event = True
+        elif event.key == K_DELETE:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                self.text_box_layout.delete_selected_text()
+                low_end = min(self.select_range[0], self.select_range[1])
+                high_end = max(self.select_range[0], self.select_range[1])
+                self.html_text = self.html_text[:low_end] + self.html_text[high_end:]
+                self.edit_position = low_end
+                self.select_range = [0, 0]
+                self.cursor_has_moved_recently = True
+                self.text_box_layout.set_cursor_position(self.edit_position)
+                self.redraw_from_text_block()
+
+            elif self.edit_position < len(self.html_text):
+                self.html_text = self.html_text[:self.edit_position] + self.html_text[self.edit_position + 1:]
+                self.edit_position = self.edit_position
+                self.cursor_has_moved_recently = True
+
+                self.text_box_layout.delete_at_cursor()
+                self.redraw_from_text_block()
+            consumed_event = True
+        elif self._process_edit_pos_move_key(event):
+            consumed_event = True
+
+        return consumed_event
+
+    def _process_edit_pos_move_key(self, event: Event) -> bool:
+        """
+        Process an action key that is moving the cursor edit position.
+
+        :param event: The event to process.
+
+        :return: True if event is consumed.
+
+        """
+        consumed_event = False
+        if event.key == K_LEFT:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                self.edit_position = min(self.select_range[0], self.select_range[1])
+                self.select_range = [0, 0]
+                self.cursor_has_moved_recently = True
+            elif self.edit_position > 0:
+                self.edit_position -= 1
+                self.cursor_has_moved_recently = True
+            consumed_event = True
+        elif event.key == K_RIGHT:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                self.edit_position = max(self.select_range[0], self.select_range[1])
+                self.select_range = [0, 0]
+                self.cursor_has_moved_recently = True
+            elif self.edit_position < len(self.html_text):
+                self.edit_position += 1
+                self.cursor_has_moved_recently = True
+            consumed_event = True
+        elif event.key == K_UP:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                self.edit_position = self.text_box_layout.get_cursor_pos_move_up_one_row(self.last_horiz_cursor_index)
+                self.select_range = [0, 0]
+                self.cursor_has_moved_recently = True
+            else:
+                self.edit_position = self.text_box_layout.get_cursor_pos_move_up_one_row(self.last_horiz_cursor_index)
+                self.cursor_has_moved_recently = True
+            self.vertical_cursor_movement = True
+            consumed_event = True
+        elif event.key == K_DOWN:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                self.edit_position = self.text_box_layout.get_cursor_pos_move_down_one_row(self.last_horiz_cursor_index)
+                self.select_range = [0, 0]
+                self.cursor_has_moved_recently = True
+            else:
+                self.edit_position = self.text_box_layout.get_cursor_pos_move_down_one_row(self.last_horiz_cursor_index)
+                self.cursor_has_moved_recently = True
+            self.vertical_cursor_movement = True
+            consumed_event = True
+        elif event.key == K_HOME:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                self.select_range = [0, 0]
+            self.edit_position = 0
+            self.cursor_has_moved_recently = True
+            consumed_event = True
+        elif event.key == K_END:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                self.select_range = [0, 0]
+            self.edit_position = len(self.html_text)
+            self.cursor_has_moved_recently = True
+            consumed_event = True
+        return consumed_event
+
+    def _process_mouse_button_event(self, event: Event) -> bool:
+        """
+        Process a mouse button event.
+
+        :param event: Event to process.
+
+        :return: True if we consumed the mouse event.
+
+        """
+        consumed_event = False
+        if event.type == MOUSEBUTTONDOWN and event.button == BUTTON_LEFT:
+            scaled_mouse_pos = self.ui_manager.calculate_scaled_mouse_position(event.pos)
+            if self.hover_point(scaled_mouse_pos[0], scaled_mouse_pos[1]):
+                if self.is_enabled:
+                    text_layout_space_pos = self._calculate_text_space_pos(scaled_mouse_pos)
+                    self.text_box_layout.set_cursor_from_click_pos(text_layout_space_pos)
+                    self.edit_position = self.text_box_layout.get_cursor_index()
+                    self.redraw_from_text_block()
+
+                    double_clicking = False
+                    if self.double_click_timer < self.ui_manager.get_double_click_time():
+                        if self._calculate_double_click_word_selection():
+                            double_clicking = True
+
+                    if not double_clicking:
+                        self.select_range = [self.edit_position, self.edit_position]
+                        self.cursor_has_moved_recently = True
+                        self.selection_in_progress = True
+                        self.double_click_timer = 0.0
+
+                consumed_event = True
+
+        if (event.type == MOUSEBUTTONUP and
+                event.button == BUTTON_LEFT and
+                self.selection_in_progress):
+            scaled_mouse_pos = self.ui_manager.calculate_scaled_mouse_position(event.pos)
+
+            if self.hover_point(scaled_mouse_pos[0], scaled_mouse_pos[1]):
+                consumed_event = True
+                text_layout_space_pos = self._calculate_text_space_pos(scaled_mouse_pos)
+                self.text_box_layout.set_cursor_from_click_pos(text_layout_space_pos)
+                new_edit_pos = self.text_box_layout.get_cursor_index()
+                if new_edit_pos != self.edit_position:
+                    self.edit_position = new_edit_pos
+                    self.cursor_has_moved_recently = True
+                    self.select_range = [self.select_range[0], self.edit_position]
+                    self.redraw_from_text_block()
+            self.selection_in_progress = False
+
+        return consumed_event
+
+    def _process_text_entry_key(self, event: Event) -> bool:
+        """
+        Process key input that can be added to the text entry text.
+
+        :param event: The event to process.
+
+        :return: True if consumed.
+        """
+        consumed_event = False
+        if event.key == K_RETURN:
+            start_str = self.html_text[:self.edit_position]
+            end_str = self.html_text[self.edit_position:]
+            self.html_text = start_str + '\n' + end_str
+            self.text_box_layout.insert_line_break(self.edit_position, self.parser)
+            self.edit_position += 1
+            self.redraw_from_text_block()
+            self.cursor_has_moved_recently = True
+            consumed_event = True
+
+        elif hasattr(event, 'unicode'):
+            character = event.unicode
+            # here we really want to get the font metrics for the current active style where the edit cursor is
+            # instead we will make do for now
+            font = self.ui_theme.get_font(self.combined_element_ids)
+            char_metrics = font.get_metrics(character)
+            if len(char_metrics) > 0 and char_metrics[0] is not None:
+                valid_character = True
+                if valid_character:
+                    if abs(self.select_range[0] - self.select_range[1]) > 0:
+                        low_end = min(self.select_range[0], self.select_range[1])
+                        high_end = max(self.select_range[0], self.select_range[1])
+                        self.html_text = self.html_text[:low_end] + character + self.html_text[high_end:]
+                        self.text_box_layout.delete_selected_text()
+                        self.text_box_layout.insert_text(character, low_end, self.parser)
+                        self.edit_position = low_end + 1
+                        self.select_range = [0, 0]
+                    else:
+                        start_str = self.html_text[:self.edit_position]
+                        end_str = self.html_text[self.edit_position:]
+                        self.html_text = start_str + character + end_str
+                        self.text_box_layout.insert_text(character, self.edit_position, self.parser)
+                        self.edit_position += 1
+                    self.redraw_from_text_block()
+                    self.cursor_has_moved_recently = True
+                    consumed_event = True
+        return consumed_event
+
+    def _process_keyboard_shortcut_event(self, event: Event) -> bool:
+        """
+        Check if event is one of the CTRL key keyboard shortcuts.
+
+        :param event: event to process.
+
+        :return: True if event consumed.
+
+        """
+        consumed_event = False
+        if (event.key == K_a and
+                (event.mod & KMOD_CTRL or event.mod & KMOD_META)):
+            self.select_range = [0, len(self.html_text)]
+            self.edit_position = len(self.html_text)
+            self.cursor_has_moved_recently = True
+            consumed_event = True
+        elif (event.key == K_x and
+              (event.mod & KMOD_CTRL or event.mod & KMOD_META)):
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                low_end = min(self.select_range[0], self.select_range[1])
+                high_end = max(self.select_range[0], self.select_range[1])
+                clipboard_copy(self.html_text[low_end:high_end])
+                self.text_box_layout.delete_selected_text()
+                self.edit_position = low_end
+                self.html_text = self.html_text[:low_end] + self.html_text[high_end:]
+                self.text_box_layout.set_cursor_position(self.edit_position)
+                self.select_range = [0, 0]
+                self.redraw_from_text_block()
+                self.cursor_has_moved_recently = True
+                consumed_event = True
+        elif (event.key == K_c and
+              (event.mod & KMOD_CTRL or event.mod & KMOD_META)):
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                low_end = min(self.select_range[0], self.select_range[1])
+                high_end = max(self.select_range[0], self.select_range[1])
+                clipboard_copy(self.html_text[low_end:high_end])
+                consumed_event = True
+        elif self._process_paste_event(event):
+            consumed_event = True
+        return consumed_event
+
+    def _process_paste_event(self, event: Event) -> bool:
+        """
+        Process a paste shortcut event. (CTRL+ V)
+
+        :param event: The event to process.
+
+        :return: True if the event is consumed.
+
+        """
+        consumed_event = False
+        if event.key == K_v and (event.mod & KMOD_CTRL or event.mod & KMOD_META):
+            new_text = self.convert_all_line_endings_to_unix(clipboard_paste())
+
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                low_end = min(self.select_range[0], self.select_range[1])
+                high_end = max(self.select_range[0], self.select_range[1])
+                self.html_text = self.html_text[:low_end] + new_text + self.html_text[high_end:]
+                self.set_text(self.html_text)
+                self.edit_position = low_end + len(new_text)
+                self.text_box_layout.set_cursor_position(self.edit_position)
+                self.redraw_from_text_block()
+                self.select_range = [0, 0]
+                self.cursor_has_moved_recently = True
+            elif len(new_text) > 0:
+                self.html_text = (self.html_text[:self.edit_position] +
+                                  new_text +
+                                  self.html_text[self.edit_position:])
+                self.set_text(self.html_text)
+                self.edit_position += len(new_text)
+                self.text_box_layout.set_cursor_position(self.edit_position)
+                self.redraw_from_text_block()
+                self.cursor_has_moved_recently = True
+            consumed_event = True
+        return consumed_event
+
+    def _calculate_double_click_word_selection(self):
+        """
+        If we double clicked on a word in the text, select that word.
+
+        """
+        if self.edit_position != self.select_range[0]:
+            return False
+        index = min(self.edit_position, len(self.html_text) - 1)
+        if index >= 0:
+            char = self.html_text[index]
+            # Check we clicked in the same place on a our second click.
+            pattern = re.compile(r"[\w']+")
+
+            while index >= 0 and not pattern.match(char):
+                index -= 1
+                if index >= 0:
+                    char = self.html_text[index]
+                else:
+                    break
+            while index >= 0 and pattern.match(char):
+                index -= 1
+                if index >= 0:
+                    char = self.html_text[index]
+                else:
+                    break
+            start_select_index = index + 1
+            index += 1
+            if index < len(self.html_text):
+                char = self.html_text[index]
+                while index < len(self.html_text) and pattern.match(char):
+                    index += 1
+                    if index < len(self.html_text):
+                        char = self.html_text[index]
+            end_select_index = index
+            self.select_range = [start_select_index, end_select_index]
+            self.edit_position = end_select_index
+            self.cursor_has_moved_recently = True
+            self.selection_in_progress = False
+            return True
+        else:
+            return False
+
+    def redraw_from_text_block(self):
+        self.text_box_layout.fit_layout_rect_height_to_rows()
+        super().redraw_from_text_block()
+
+    @staticmethod
+    def convert_all_line_endings_to_unix(input_str: str):
+        return input_str.replace('\r\n', '\n').replace('\r', '\n')
+
diff --git a/pygame_gui/elements/ui_text_entry_line.py b/pygame_gui/elements/ui_text_entry_line.py
index ae89cd71..e507dd93 100644
--- a/pygame_gui/elements/ui_text_entry_line.py
+++ b/pygame_gui/elements/ui_text_entry_line.py
@@ -1,7 +1,7 @@
 import re
 import warnings
 
-from typing import Union, List, Dict, Optional
+from typing import Union, List, Dict, Optional, Tuple
 
 import pygame
 
@@ -67,7 +67,7 @@ class UITextEntryLine(UIElement):
                                         _number_character_set['en'])}
 
     def __init__(self,
-                 relative_rect: pygame.Rect,
+                 relative_rect: Union[pygame.Rect, Tuple[int, int, int, int]],
                  manager: Optional[IUIManagerInterface] = None,
                  container: Optional[IContainerLikeInterface] = None,
                  parent_element: Optional[UIElement] = None,
@@ -307,16 +307,14 @@ def update(self, time_delta: float):
         if not self.alive():
             return
         scaled_mouse_pos = self.ui_manager.get_mouse_position()
-        if self.hover_point(scaled_mouse_pos[0], scaled_mouse_pos[1]):
+        if self.hovered:
             self.ui_manager.set_text_input_hovered(True)
-        else:
-            self.ui_manager.set_text_input_hovered(False)
+
         if self.double_click_timer < self.ui_manager.get_double_click_time():
             self.double_click_timer += time_delta
         if self.selection_in_progress:
-            mouse_pos = self.ui_manager.get_mouse_position()
-            drawable_shape_space_click = (mouse_pos[0] - self.rect.left,
-                                          mouse_pos[1] - self.rect.top)
+            drawable_shape_space_click = (scaled_mouse_pos[0] - self.rect.left,
+                                          scaled_mouse_pos[1] - self.rect.top)
             if self.drawable_shape is not None:
                 self.drawable_shape.text_box_layout.set_cursor_from_click_pos(
                     drawable_shape_space_click)
@@ -774,8 +772,8 @@ def _calculate_double_click_word_selection(self):
         if index >= 0:
             char = self.text[index]
             # Check we clicked in the same place on a our second click.
+            pattern = re.compile(r"[\w']+")
             if index > 0:
-                pattern = re.compile(r"[\w']+")
                 while not pattern.match(char):
                     index -= 1
                     if index > 0:
@@ -797,7 +795,6 @@ def _calculate_double_click_word_selection(self):
                     if index < len(self.text):
                         char = self.text[index]
             end_select_index = index
-            print([start_select_index, end_select_index])
             self.select_range = [start_select_index, end_select_index]
             self.edit_position = end_select_index
             self.cursor_has_moved_recently = True
diff --git a/pygame_gui/elements/ui_vertical_scroll_bar.py b/pygame_gui/elements/ui_vertical_scroll_bar.py
index 49327613..c11c96b0 100644
--- a/pygame_gui/elements/ui_vertical_scroll_bar.py
+++ b/pygame_gui/elements/ui_vertical_scroll_bar.py
@@ -56,7 +56,7 @@ def __init__(self,
         self.top_limit = 0.0
         self.starting_grab_y_difference = 0
         self.visible_percentage = max(0.0, min(visible_percentage, 1.0))
-        self.start_percentage = 0.0
+        self._start_percentage = 0.0
 
         self.grabbed_slider = False
         self.has_moved_recently = False
@@ -107,6 +107,18 @@ def __init__(self,
         self.join_focus_sets(self.sliding_button)
         self.sliding_button.set_hold_range((100, self.background_rect.height))
 
+    @property
+    def start_percentage(self):
+        """
+        turning start_percentage into a property so we can round it to mitigate floating point errors
+        """
+        return self._start_percentage
+
+    @start_percentage.setter
+    def start_percentage(self, value):
+        rounded_value = round(value, 4)
+        self._start_percentage = rounded_value
+
     def rebuild(self):
         """
         Rebuild anything that might need rebuilding.
@@ -330,7 +342,8 @@ def update(self, time_delta: float):
                 self.grabbed_slider = False
 
             if moved_this_frame:
-                self.start_percentage = self.scroll_position / self.scrollable_height
+                self.start_percentage = min(self.scroll_position / self.scrollable_height,
+                                            1.0 - self.visible_percentage)
                 if not self.has_moved_recently:
                     self.has_moved_recently = True
 
@@ -365,7 +378,7 @@ def redraw_scrollbar(self):
         scroll_bar_height = max(5, int(self.scrollable_height * self.visible_percentage))
 
         x_pos = 0
-        y_pos = (self.scroll_position + self.arrow_button_height)
+        y_pos = min((self.bottom_limit + self.arrow_button_height) - scroll_bar_height, (self.scroll_position + self.arrow_button_height))
         self.sliding_rect_position = pygame.math.Vector2(x_pos, y_pos)
 
         self.sliding_button.set_relative_position(self.sliding_rect_position)
diff --git a/pygame_gui/ui_manager.py b/pygame_gui/ui_manager.py
index 086d7900..31ecb001 100644
--- a/pygame_gui/ui_manager.py
+++ b/pygame_gui/ui_manager.py
@@ -264,6 +264,8 @@ def update(self, time_delta: float):
         self._update_mouse_position()
         self._handle_hovering(time_delta)
 
+        self.set_text_input_hovered(False)  # reset the text hovered status each loop
+
         self.ui_group.update(time_delta)
 
         # handle mouse cursors


Test patch:
diff --git a/pygame_gui/core/text/simple_test_layout_rect.py b/pygame_gui/core/text/simple_test_layout_rect.py
index 05f2bd7d..c552882c 100644
--- a/pygame_gui/core/text/simple_test_layout_rect.py
+++ b/pygame_gui/core/text/simple_test_layout_rect.py
@@ -56,7 +56,7 @@ def finalise(self,
         surface.fill(self.colour)
         target_surface.blit(surface, self, area=target_area)
 
-    def split(self, requested_x: int, line_width: int, row_start_x: int):
+    def split(self, requested_x: int, line_width: int, row_start_x: int, allow_split_dashes: bool = True):
 
         if line_width < self.smallest_split_size:
             raise ValueError('Line width is too narrow')
diff --git a/tests/test_core/test_text/test_html_parser.py b/tests/test_core/test_text/test_html_parser.py
index 7b5802e0..954ac9ad 100644
--- a/tests/test_core/test_text/test_html_parser.py
+++ b/tests/test_core/test_text/test_html_parser.py
@@ -71,11 +71,11 @@ def test_handle_start_tag(self, _init_pygame, default_ui_manager: UIManager):
 
         parser.handle_starttag(tag='font', attrs=[('pixel_size', "")])
 
-        assert parser.current_style['font_size'] is None
+        assert parser.current_style['font_size'] is 14
 
         parser.handle_starttag(tag='font', attrs=[('size', "")])
 
-        assert parser.current_style['font_size'] is None
+        assert parser.current_style['font_size'] is 14
 
         with pytest.warns(UserWarning, match="not a supported html style size"):
             parser.handle_starttag(tag='font', attrs=[('size', "8")])
@@ -92,7 +92,7 @@ def test_handle_start_tag(self, _init_pygame, default_ui_manager: UIManager):
         parser.handle_starttag(tag='p', attrs=[])
         parser.handle_starttag(tag='p', attrs=[])
 
-        assert isinstance(parser.layout_rect_queue[-1], LineBreakLayoutRect)
+        assert isinstance(parser.layout_rect_queue[-2], LineBreakLayoutRect)
 
         parser.handle_starttag(tag='img', attrs=[('src', 'tests/data/images/splat.png'),
                                                  ('float', 'none'),
diff --git a/tests/test_core/test_text/test_line_break_layout_rect.py b/tests/test_core/test_text/test_line_break_layout_rect.py
index 8d3d15a0..3e7f383f 100644
--- a/tests/test_core/test_text/test_line_break_layout_rect.py
+++ b/tests/test_core/test_text/test_line_break_layout_rect.py
@@ -8,13 +8,15 @@
 
 class TestLineBreakLayoutRect:
     def test_creation(self, _init_pygame, default_ui_manager: UIManager):
-        line_break_rect = LineBreakLayoutRect(dimensions=(200, 30))
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        line_break_rect = LineBreakLayoutRect(dimensions=(200, 30), font=default_font)
 
         assert line_break_rect.width == 200
         assert line_break_rect.height == 30
 
     def test_finalise(self, _init_pygame, default_ui_manager: UIManager):
-        line_break_rect = LineBreakLayoutRect(dimensions=(200, 30))
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        line_break_rect = LineBreakLayoutRect(dimensions=(200, 30), font=default_font)
 
         rendered_chunk_surf = pygame.Surface((200, 30))
         rendered_chunk_surf.fill((0, 0, 0))
diff --git a/tests/test_core/test_text/test_text_box_layout.py b/tests/test_core/test_text/test_text_box_layout.py
index 536b883f..868b7ebc 100644
--- a/tests/test_core/test_text/test_text_box_layout.py
+++ b/tests/test_core/test_text/test_text_box_layout.py
@@ -14,10 +14,16 @@
 class TestTextBoxLayout:
     def test_creation(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                      line_spacing=1.0,
+                      default_font_data=default_font_data)
 
     def test_creation_with_data(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([SimpleTestLayoutRect(dimensions=(50, 20)),
@@ -61,11 +67,16 @@ def test_creation_with_data(self, _init_pygame, default_ui_manager: UIManager):
                             SimpleTestLayoutRect(dimensions=(30, 20)),
                             SimpleTestLayoutRect(dimensions=(30, 20))
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 200, 110),
                                view_rect=pygame.Rect(0, 0, 200, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         assert len(layout.layout_rows) > 0
 
@@ -79,7 +90,8 @@ def test_creation_with_data(self, _init_pygame, default_ui_manager: UIManager):
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 110),
                                view_rect=pygame.Rect(0, 0, 200, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         assert len(layout.layout_rows) > 0
 
@@ -93,7 +105,8 @@ def test_creation_with_data(self, _init_pygame, default_ui_manager: UIManager):
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 110),
                                view_rect=pygame.Rect(0, 0, 200, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         assert len(layout.layout_rows) > 0
 
@@ -116,7 +129,8 @@ def test_creation_with_data(self, _init_pygame, default_ui_manager: UIManager):
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 90),
                                view_rect=pygame.Rect(0, 0, 200, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         assert len(layout.layout_rows) > 0
 
@@ -132,10 +146,16 @@ def test_too_wide_image(self, _init_pygame, default_ui_manager: UIManager):
                                             padding=Padding(0, 0, 0, 0))])
 
         with pytest.warns(UserWarning, match="too wide for text layout"):
+            default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+            default_font_data = {"font": default_font,
+                                 "font_colour": pygame.Color("#FFFFFF"),
+                                 "bg_colour": pygame.Color("#00000000")
+                                 }
             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)
+                          line_spacing=1.0,
+                          default_font_data=default_font_data)
 
     def test_reprocess_layout_queue(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([SimpleTestLayoutRect(dimensions=(50, 20)),
@@ -146,11 +166,16 @@ def test_reprocess_layout_queue(self, _init_pygame, default_ui_manager: UIManage
                             SimpleTestLayoutRect(dimensions=(30, 20)),
                             SimpleTestLayoutRect(dimensions=(90, 20)),
                             SimpleTestLayoutRect(dimensions=(175, 20))])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         assert len(layout.layout_rows) == 4
         layout.reprocess_layout_queue(pygame.Rect(0, 0, 100, 300))
@@ -166,10 +191,16 @@ def test_finalise_to_surf(self, _init_pygame, default_ui_manager: UIManager):
                             SimpleTestLayoutRect(dimensions=(90, 20)),
                             SimpleTestLayoutRect(dimensions=(175, 20))])
 
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout_surface = pygame.Surface((200, 300), depth=32, flags=pygame.SRCALPHA)
         layout_surface.fill((0, 0, 0, 0))
@@ -187,10 +218,16 @@ def test_finalise_to_new(self, _init_pygame, default_ui_manager: UIManager):
                             SimpleTestLayoutRect(dimensions=(90, 20)),
                             SimpleTestLayoutRect(dimensions=(175, 20))])
 
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout_surface = layout.finalise_to_new()
 
@@ -204,10 +241,16 @@ def test_update_text_with_new_text_end_pos(self, _init_pygame, default_ui_manage
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000'))])
 
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout_surface = layout.finalise_to_new()
         layout.update_text_with_new_text_end_pos(0)  # this does nothing unless we pass in text
@@ -226,10 +269,16 @@ def test_clear_final_surface(self,  _init_pygame, default_ui_manager: UIManager)
                             SimpleTestLayoutRect(dimensions=(90, 20)),
                             SimpleTestLayoutRect(dimensions=(175, 20))])
 
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout_surface = layout.finalise_to_new()
 
@@ -247,10 +296,16 @@ def test_set_alpha(self,  _init_pygame, default_ui_manager: UIManager):
                             SimpleTestLayoutRect(dimensions=(90, 20)),
                             SimpleTestLayoutRect(dimensions=(175, 20))])
 
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout_surface = layout.finalise_to_new()
 
@@ -281,10 +336,16 @@ def test_add_chunks_to_hover_group(self, _init_pygame, default_ui_manager: UIMan
                                                hover_underline=False)
                             ])
 
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         links_found = []
         layout.add_chunks_to_hover_group(links_found)
@@ -312,10 +373,16 @@ def test_insert_layout_rects(self, _init_pygame, default_ui_manager: UIManager):
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
 
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 300, 150),
                                view_rect=pygame.Rect(0, 0, 300, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         insert_data = deque([TextLineChunkFTFont(text='n insertion',
                                                  font=the_font,
@@ -330,9 +397,9 @@ def test_insert_layout_rects(self, _init_pygame, default_ui_manager: UIManager):
                                    chunk_index=9)
 
         row = layout.layout_rows[0]
-        chunk = row.items[1]
+        chunk = row.items[0]
 
-        assert chunk.text == 'this is an insertion'
+        assert chunk.text == 'hello this is an insertion test'
 
         layout_surface = layout.finalise_to_new()
 
@@ -399,11 +466,16 @@ def test_horiz_centre_all_rows(self, _init_pygame, default_ui_manager: UIManager
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 500, 300),
                                view_rect=pygame.Rect(0, 0, 500, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         row = layout.layout_rows[0]
         assert row.x == 0
@@ -434,10 +506,16 @@ def test_align_left_all_rows(self, _init_pygame, default_ui_manager: UIManager):
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
 
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 500, 300),
                                view_rect=pygame.Rect(0, 0, 500, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         row = layout.layout_rows[0]
         assert row.x == 0
@@ -471,11 +549,16 @@ def test_align_right_all_rows(self, _init_pygame, default_ui_manager: UIManager)
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 500, 300),
                                view_rect=pygame.Rect(0, 0, 500, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         row = layout.layout_rows[0]
         assert row.x == 0
@@ -504,11 +587,16 @@ def test_vert_center_all_rows(self, _init_pygame, default_ui_manager: UIManager)
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 500, 300),
                                view_rect=pygame.Rect(0, 0, 500, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         row = layout.layout_rows[0]
         assert row.y == 0
@@ -537,11 +625,16 @@ def test_vert_align_top_all_rows(self, _init_pygame, default_ui_manager: UIManag
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 500, 300),
                                view_rect=pygame.Rect(0, 0, 500, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         row = layout.layout_rows[0]
         assert row.y == 0
@@ -571,11 +664,16 @@ def test_vert_align_bottom_all_rows(self, _init_pygame, default_ui_manager: UIMa
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 500, 300),
                                view_rect=pygame.Rect(0, 0, 500, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         row = layout.layout_rows[0]
         assert row.y == 0
@@ -603,11 +701,16 @@ def test_set_cursor_position(self, _init_pygame, default_ui_manager: UIManager):
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 300),
                                view_rect=pygame.Rect(0, 0, 100, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.set_cursor_position(13)
 
@@ -639,11 +742,16 @@ def test_set_cursor_from_click_pos(self, _init_pygame, default_ui_manager: UIMan
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 300),
                                view_rect=pygame.Rect(0, 0, 100, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.set_cursor_from_click_pos((17, 24))
         assert layout.cursor_text_row is not None
@@ -674,11 +782,16 @@ def test_toggle_cursor(self, _init_pygame, default_ui_manager: UIManager):
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 300),
                                view_rect=pygame.Rect(0, 0, 100, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.set_cursor_from_click_pos((17, 24))
 
@@ -716,11 +829,16 @@ def test_set_text_selection(self, _init_pygame, default_ui_manager: UIManager):
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000'))
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 300),
                                view_rect=pygame.Rect(0, 0, 100, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.set_text_selection(5, 17)
 
@@ -756,11 +874,16 @@ def test_set_default_text_colour(self, _init_pygame, default_ui_manager: UIManag
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 500, 300),
                                view_rect=pygame.Rect(0, 0, 500, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.set_default_text_colour(pygame.Color('#00FF00'))
         default_chunk_colour = layout.layout_rows[0].items[1].colour
@@ -787,25 +910,31 @@ def test_insert_text(self, _init_pygame, default_ui_manager: UIManager):
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 500, 300),
                                view_rect=pygame.Rect(0, 0, 500, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.insert_text('nother insertion', 15)
 
         row = layout.layout_rows[0]
-        chunk = row.items[1]
+        chunk = row.items[0]
 
-        assert chunk.text == 'this is another insertion'
+        assert chunk.text == 'hello this is another insertion test'
 
         layout.insert_text('text on the end', 51)  # deliberately high number that should get clamped
 
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 500, 300),
                                view_rect=pygame.Rect(0, 0, 500, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.layout_rows = []
 
@@ -833,11 +962,16 @@ def test_delete_selected_text(self, _init_pygame, default_ui_manager: UIManager)
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 300),
                                view_rect=pygame.Rect(0, 0, 100, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.set_text_selection(5, 17)
 
@@ -923,14 +1057,21 @@ def test_delete_at_cursor(self, _init_pygame, default_ui_manager: UIManager):
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 300),
                                view_rect=pygame.Rect(0, 0, 100, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.set_cursor_position(14)
         layout.delete_at_cursor()
+        # need to reset the cursor position before deleting again as chunks will have merged and been re-processed
+        layout.set_cursor_position(14)
         layout.delete_at_cursor()
 
         remaining_text = ''
@@ -961,14 +1102,21 @@ def test_backspace_at_cursor(self, _init_pygame, default_ui_manager: UIManager):
                                                 using_default_text_colour=False,
                                                 bg_colour=pygame.Color('#FF0000')),
                             ])
-
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         layout = TextBoxLayout(input_data_queue=input_data,
                                layout_rect=pygame.Rect(0, 0, 100, 300),
                                view_rect=pygame.Rect(0, 0, 100, 150),
-                               line_spacing=1.0)
+                               line_spacing=1.0,
+                               default_font_data=default_font_data)
 
         layout.set_cursor_position(16)
         layout.backspace_at_cursor()
+        # need to reset the cursor position before backspacing again as chunks will have merged and been re-processed
+        layout.set_cursor_position(15)
         layout.backspace_at_cursor()
 
         remaining_text = ''
diff --git a/tests/test_core/test_text/test_text_box_layout_row.py b/tests/test_core/test_text/test_text_box_layout_row.py
index 98b33c3e..012bf009 100644
--- a/tests/test_core/test_text/test_text_box_layout_row.py
+++ b/tests/test_core/test_text/test_text_box_layout_row.py
@@ -12,10 +12,16 @@
 class TestTextBoxLayoutRow:
     def test_creation(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=1.0,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -27,10 +33,16 @@ def test_creation(self, _init_pygame, default_ui_manager: UIManager):
 
     def test_at_start(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=1.0,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -45,10 +57,16 @@ def test_at_start(self, _init_pygame, default_ui_manager: UIManager):
     def test_add_item(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=1.0,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -66,10 +84,16 @@ def test_add_item(self, _init_pygame, default_ui_manager: UIManager):
     def test_rewind_row(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=1.0,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -93,10 +117,16 @@ def test_rewind_row(self, _init_pygame, default_ui_manager: UIManager):
     def test_horiz_center_row(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -129,10 +159,16 @@ def test_horiz_center_row(self, _init_pygame, default_ui_manager: UIManager):
     def test_align_left_row(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -159,10 +195,16 @@ def test_align_left_row(self, _init_pygame, default_ui_manager: UIManager):
     def test_align_right_row(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -198,10 +240,16 @@ def test_align_right_row(self, _init_pygame, default_ui_manager: UIManager):
     def test_vert_align_items_to_row(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -248,10 +296,16 @@ def test_vert_align_items_to_row(self, _init_pygame, default_ui_manager: UIManag
     def test_merge_adjacent_compatible_chunks(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -291,10 +345,16 @@ def test_merge_adjacent_compatible_chunks(self, _init_pygame, default_ui_manager
     def test_finalise(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -334,10 +394,16 @@ def test_finalise(self, _init_pygame, default_ui_manager: UIManager):
     def test_set_default_text_colour(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -367,10 +433,16 @@ def test_set_default_text_colour(self, _init_pygame, default_ui_manager: UIManag
     def test_toggle_cursor(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -408,10 +480,16 @@ def test_toggle_cursor(self, _init_pygame, default_ui_manager: UIManager):
     def test_clear(self, _init_pygame, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -437,13 +515,19 @@ def test_clear(self, _init_pygame, default_ui_manager: UIManager):
         layout_row.clear()
         assert layout_surface.get_at((18, 18)) == pygame.Color('#00000000')
 
-    def test_set_cursor_from_click_pos(self):
+    def test_set_cursor_from_click_pos(self, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -471,7 +555,7 @@ def test_set_cursor_from_click_pos(self):
         assert layout_surface.get_at((1, 5)) == pygame.Color('#FFFFFF')
         assert layout_row.cursor_index == 0
 
-        layout_row.set_cursor_from_click_pos((44, 5))
+        layout_row.set_cursor_from_click_pos((44, 5), num_rows=1)
 
         layout_row.toggle_cursor()
         layout_row.toggle_cursor()
@@ -481,22 +565,28 @@ def test_set_cursor_from_click_pos(self):
         assert layout_row.cursor_index == 3
         assert layout_row.cursor_draw_width == 44
 
-        layout_row.set_cursor_from_click_pos((180, 5))
+        layout_row.set_cursor_from_click_pos((180, 5), num_rows=1)
 
         assert layout_row.cursor_index == 4
 
-        layout_row.set_cursor_from_click_pos((-1, 5))
+        layout_row.set_cursor_from_click_pos((-1, 5), num_rows=1)
 
         assert layout_row.left == 0
         assert layout_row.cursor_index == 0
 
-    def test_set_cursor_position(self):
+    def test_set_cursor_position(self, default_ui_manager: UIManager):
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
@@ -551,10 +641,16 @@ def test_insert_text(self, _init_pygame, default_ui_manager: UIManager):
 
         input_data = deque([])
         line_spacing = 1.25
+        default_font = default_ui_manager.get_theme().get_font_dictionary().get_default_font()
+        default_font_data = {"font": default_font,
+                             "font_colour": pygame.Color("#FFFFFF"),
+                             "bg_colour": pygame.Color("#00000000")
+                             }
         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)
+                                        line_spacing=line_spacing,
+                                        default_font_data=default_font_data)
         layout_row = TextBoxLayoutRow(row_start_x=0,
                                       row_start_y=0,
                                       row_index=0,
diff --git a/tests/test_elements/test_ui_text_entry_box.py b/tests/test_elements/test_ui_text_entry_box.py
new file mode 100644
index 00000000..9fb32ea7
--- /dev/null
+++ b/tests/test_elements/test_ui_text_entry_box.py
@@ -0,0 +1,1465 @@
+import os
+
+import pygame
+import pytest
+import platform
+
+import pygame_gui
+
+from pygame_gui.core.utility import clipboard_paste, clipboard_copy
+from pygame_gui.elements.ui_text_entry_box import UITextEntryBox
+from pygame_gui.ui_manager import UIManager
+from pygame_gui.core import UIContainer
+
+
+class TestUITextEntryBox:
+
+    def test_creation(self, _init_pygame: None,
+                      default_ui_manager: UIManager,
+                      _display_surface_return_none):
+        default_ui_manager.preload_fonts([{"name": "fira_code", "size:": 14, "style": "bold"},
+                                          {"name": "fira_code", "size:": 14, "style": "italic"}])
+        text_box = UITextEntryBox(
+            initial_text="<font color=#FF0000>Some text</font> in a <b>bold box</b> using colours and "
+                         "<i>styles</i>.",
+            relative_rect=pygame.Rect(100, 100, 200, 300),
+            manager=default_ui_manager)
+        assert text_box.image is not None
+
+    def test_set_text(self, _init_pygame: None,
+                      default_ui_manager: UIManager,
+                      _display_surface_return_none):
+        default_ui_manager.preload_fonts([{"name": "fira_code", "size:": 14, "style": "bold"},
+                                          {"name": "fira_code", "size:": 14, "style": "italic"}])
+        text_box = UITextEntryBox(
+            initial_text="<font color=#FF0000>Some text</font> in a <b>bold box</b> using colours and "
+                         "<i>styles</i>.",
+            relative_rect=pygame.Rect(100, 100, 200, 300),
+            manager=default_ui_manager)
+        assert text_box.image is not None
+
+        text_box.set_text("<b>Changed text</b>")
+        assert text_box.image is not None
+
+    def test_clear(self, _init_pygame: None,
+                   default_ui_manager: UIManager,
+                   _display_surface_return_none):
+        default_ui_manager.preload_fonts([{"name": "fira_code", "size:": 14, "style": "bold"},
+                                          {"name": "fira_code", "size:": 14, "style": "italic"}])
+        text_box = UITextEntryBox(
+            initial_text="<font color=#FF0000>Some text</font> in a <b>bold box</b> using colours and "
+                         "<i>styles</i>.",
+            relative_rect=pygame.Rect(100, 100, 200, 300),
+            manager=default_ui_manager)
+        assert text_box.image is not None
+
+        text_box.clear()
+        assert text_box.image is not None
+
+    def test_creation_grow_to_fit_width(self, _init_pygame: None,
+                                        default_ui_manager: UIManager,
+                                        _display_surface_return_none):
+        default_ui_manager.preload_fonts([{"name": "fira_code", "size:": 14, "style": "bold"},
+                                          {"name": "fira_code", "size:": 14, "style": "italic"}])
+        text_box = UITextEntryBox(
+            initial_text="Some text in a  box not using colours and styles. Hey hey hey, what is this?"
+                         "More text padding this out a little. Well OK.",
+            relative_rect=pygame.Rect(100, 100, -1, 50),
+            manager=default_ui_manager)
+        assert text_box.image is not None and text_box.rect.width == 984
+
+    def test_creation_and_rebuild_with_scrollbar(self, _init_pygame: None,
+                                                 default_ui_manager: UIManager,
+                                                 _display_surface_return_none):
+        default_ui_manager.preload_fonts([{'name': 'fira_code', 'html_size': 4.5, 'style': 'bold'},
+                                          {'name': 'fira_code', 'html_size': 4.5, 'style': 'regular'},
+                                          {'name': 'fira_code', 'html_size': 2, 'style': 'regular'},
+                                          {'name': 'fira_code', 'html_size': 2, 'style': 'italic'},
+                                          {'name': 'fira_code', 'html_size': 6, 'style': 'bold'},
+                                          {'name': 'fira_code', 'html_size': 6, 'style': 'regular'},
+                                          {'name': 'fira_code', 'html_size': 6, 'style': 'bold_italic'},
+                                          {'name': 'fira_code', 'html_size': 4, 'style': 'bold'},
+                                          {'name': 'fira_code', 'html_size': 4, 'style': 'italic'},
+                                          {'name': 'fira_code', 'html_size': 2, 'style': 'bold'},
+                                          {'name': 'fira_code', 'html_size': 2, 'style': 'bold_italic'}])
+        text_box = UITextEntryBox(initial_text=''
+                                               '<font color=regular_text><font color=#E784A2 size=4.5><br><b><u>Lorem</u>'
+                                               '<br><br><br>'
+                                               'ipsum dolor sit amet</b></font>,'
+                                               ' <b><a href="test">consectetur</a></b> adipiscing elit. in a flibb de '
+                                               'dib do '
+                                               '<p>rub a la clob slip the perry tin fo glorp yip dorp'
+                                               'skorp si pork flum de dum be '
+                                               '<shadow size=1 offset=0,0 color=#306090>dung</shadow>, '
+                                               'slob be robble glurp destination flum'
+                                               ' kin slum. </p>'
+                                               'Ram slim gordo, fem tulip squirrel slippers save socks certainly.<br>'
+                                               'Vestibulum in <i>commodo me</i> tellus in nisi finibus a sodales.<br>'
+                                               'Vestibulum'
+                                               '<font size=2>hendrerit mi <i>sed nulla</i> scelerisque</font>, posuere '
+                                               'ullamcorper '
+                                               'sem pulvinar.'
+                                               'Nulla at pulvinar a odio, a dictum dolor.<br>Maecenas at <font size=6><b>'
+                                               'tellus a'
+                                               'tortor. a<br>'
+                                               '<img src="tests/data/images/test_emoji.png" float=left '
+                                               'padding="5px 10px 5px 5px">'
+                                               '<img src="tests/data/images/test_emoji.png" float=right '
+                                               'padding="5px 10px">'
+                                               '<img src="tests/data/images/test_emoji.png" '
+                                               'padding="5px">'
+                                               'In <i>bibendum</i> orci et velit</b> gravida lacinia.<br><br>'
+                                               'In hac a habitasse to platea dictumst.<br>'
+                                               '<font color=#4CD656 size=4>Vivamus I interdum mollis lacus nec porttitor.'
+                                               '<br>Morbi '
+                                               'accumsan, lectus at '
+                                               'tincidunt to dictum, neque <font color=#879AF6>erat tristique erat</font>, '
+                                               'sed a tempus for <b>nunc</b> dolor in nibh.<br>'
+                                               'Suspendisse in viverra dui <i>fringilla dolor laoreet</i>, sit amet on '
+                                               'pharetra a ante '
+                                               'sollicitudin.</font></font>'
+                                               '<br><br>'
+                                               '<b>consectetur</b> adipiscing elit. in a<br>'
+                                               'Vestibulum in <i>commodo me</i> tellus in nisi finibus a sodales.<br>'
+                                               'Vestibulum <font size=2>hendrerit mi <i>sed nulla</i> scelerisque</font>, '
+                                               'posuere ullamcorper sem pulvinar. '
+                                               'Nulla at pulvinar a odio, a dictum dolor.<br>'
+                                               'Maecenas at <font size=6><b>tellus a tortor. a<br>'
+                                               'In <i>bibendum</i> orci et velit</b> gravida lacinia.<br><br>'
+                                               'In hac a habitasse to platea dictumst.<br>'
+                                               '<font color=#4CD656 size=4>Vivamus I interdum mollis lacus nec '
+                                               'porttitor.<br>Morbi '
+                                               'accumsan, lectus at'
+                                               'tincidunt to dictum, neque <font color=#879AF6>erat tristique erat</font>, '
+                                               'sed a tempus for <b>nunc</b> dolor in nibh.<br>'
+                                               'Suspendisse in viverra dui <i>fringilla dolor laoreet</i>, sit amet on '
+                                               'pharetra a ante '
+                                               'sollicitudin.</font></font>',
+                                  relative_rect=pygame.Rect(100, 100, 200, 300),
+                                  manager=default_ui_manager)
+
+        text_box.rebuild()
+
+        assert text_box.image is not None
+
+    def test_create_too_narrow_textbox_for_font(self, _init_pygame: None,
+                                                default_ui_manager: UIManager,
+                                                _display_surface_return_none):
+        # narrow text boxes are fine with no dashes between split words
+
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               '<a>alalalala</a> alalalalalal alal'
+                                               'alalalala alala alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 5, 50),
+                                  manager=default_ui_manager)
+
+        assert text_box.image is not None
+
+    def test_kill(self, _init_pygame: None, default_ui_manager: UIManager,
+                  _display_surface_return_none):
+        default_ui_manager.preload_fonts([{'name': 'fira_code', 'html_size': 4.5, 'style': 'bold'},
+                                          {'name': 'fira_code', 'html_size': 4.5, 'style': 'regular'},
+                                          {'name': 'fira_code', 'html_size': 2, 'style': 'regular'},
+                                          {'name': 'fira_code', 'html_size': 2, 'style': 'italic'},
+                                          {'name': 'fira_code', 'html_size': 6, 'style': 'bold'},
+                                          {'name': 'fira_code', 'html_size': 6, 'style': 'regular'},
+                                          {'name': 'fira_code', 'html_size': 6, 'style': 'bold_italic'},
+                                          {'name': 'fira_code', 'html_size': 4, 'style': 'bold'},
+                                          {'name': 'fira_code', 'html_size': 4, 'style': 'italic'},
+                                          {'name': 'fira_code', 'html_size': 2, 'style': 'bold'},
+                                          {'name': 'fira_code', 'html_size': 2, 'style': 'bold_italic'}])
+        text_box = UITextEntryBox(initial_text=''
+                                               '<font color=regular_text><font color=#E784A2 size=4.5><br><b><u>Lorem</u>'
+                                               '<br><br><br>'
+                                               'ipsum dolor sit amet</b></font>,'
+                                               ' <b><a href="test">consectetur</a></b> adipiscing elit. in a flibb de '
+                                               'dib do '
+                                               'rub a la clob slip the perry tin fo glorp yip dorp'
+                                               'skorp si pork flum de dum be dung, slob be robble glurp destination flum '
+                                               'kin slum. '
+                                               'Ram slim gordo, fem tulip squirrel slippers save socks certainly.<br>'
+                                               'Vestibulum in <i>commodo me</i> tellus in nisi finibus a sodales.<br>'
+                                               'Vestibulum'
+                                               '<font size=2>hendrerit mi <i>sed nulla</i> scelerisque</font>, posuere '
+                                               'ullamcorper '
+                                               'sem pulvinar.'
+                                               'Nulla at pulvinar a odio, a dictum dolor.<br>Maecenas at <font size=6><b>'
+                                               'tellus a'
+                                               'tortor. a<br>'
+                                               'In <i>bibendum</i> orci et velit</b> gravida lacinia.<br><br>'
+                                               'In hac a habitasse to platea dictumst.<br>'
+                                               '<font color=#4CD656 size=4>Vivamus I interdum mollis lacus nec porttitor.'
+                                               '<br>Morbi '
+                                               'accumsan, lectus at '
+                                               'tincidunt to dictum, neque <font color=#879AF6>erat tristique erat</font>, '
+                                               'sed a tempus for <b>nunc</b> dolor in nibh.<br>'
+                                               'Suspendisse in viverra dui <i>fringilla dolor laoreet</i>, sit amet on '
+                                               'pharetra a ante '
+                                               'sollicitudin.</font></font>'
+                                               '<br><br>'
+                                               '<b>consectetur</b> adipiscing elit. in a<br>'
+                                               'Vestibulum in <i>commodo me</i> tellus in nisi finibus a sodales.<br>'
+                                               'Vestibulum <font size=2>hendrerit mi <i>sed nulla</i> scelerisque</font>, '
+                                               'posuere ullamcorper sem pulvinar. '
+                                               'Nulla at pulvinar a odio, a dictum dolor.<br>'
+                                               'Maecenas at <font size=6><b>tellus a tortor. a<br>'
+                                               'In <i>bibendum</i> orci et velit</b> gravida lacinia.<br><br>'
+                                               'In hac a habitasse to platea dictumst.<br>'
+                                               '<font color=#4CD656 size=4>Vivamus I interdum mollis lacus nec '
+                                               'porttitor.<br>Morbi '
+                                               'accumsan, lectus at'
+                                               'tincidunt to dictum, neque <font color=#879AF6>erat tristique erat</font>, '
+                                               'sed a tempus for <b>nunc</b> dolor in nibh.<br>'
+                                               'Suspendisse in viverra dui <i>fringilla dolor laoreet</i>, sit amet '
+                                               'on pharetra a ante '
+                                               'sollicitudin.</font></font>',
+                                  relative_rect=pygame.Rect(100, 100, 200, 300),
+                                  manager=default_ui_manager)
+
+        assert len(default_ui_manager.get_root_container().elements) == 3
+        assert len(default_ui_manager.get_sprite_group().sprites()) == 7
+        assert default_ui_manager.get_sprite_group().sprites() == [default_ui_manager.get_root_container(),
+                                                                   text_box,
+                                                                   text_box.scroll_bar,
+                                                                   text_box.scroll_bar.button_container,
+                                                                   text_box.scroll_bar.top_button,
+                                                                   text_box.scroll_bar.bottom_button,
+                                                                   text_box.scroll_bar.sliding_button]
+        text_box.kill()
+        text_box.update(0.01)
+        assert len(default_ui_manager.get_root_container().elements) == 0
+        assert len(default_ui_manager.get_sprite_group().sprites()) == 1
+        assert default_ui_manager.get_sprite_group().sprites() == [default_ui_manager.get_root_container()]
+
+    def test_on_fresh_drawable_shape_ready(self, _init_pygame: None,
+                                           default_ui_manager: UIManager,
+                                           _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala alala alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 100, 100),
+                                  manager=default_ui_manager)
+        text_box.on_fresh_drawable_shape_ready()
+
+        assert text_box.background_surf is not None
+
+    def test_set_position_with_scrollbar(self, _init_pygame: None,
+                                         default_ui_manager: UIManager,
+                                         _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala alala alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 100, 100),
+                                  manager=default_ui_manager)
+        text_box.set_position(pygame.math.Vector2(0.0, 0.0))
+        assert text_box.rect.topleft == (0, 0)
+
+        default_ui_manager.process_events(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                             {'button': 1,
+                                                              'pos': (92, 8)}))
+        # if we successfully clicked on the moved text box scroll bar then this button should be True
+        assert text_box.scroll_bar.top_button.held is True
+
+    def test_set_relative_position_with_scrollbar(self, _init_pygame: None,
+                                                  default_ui_manager: UIManager,
+                                                  _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala alala alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        text_box.set_relative_position(pygame.math.Vector2(0.0, 0.0))
+        assert text_box.rect.topleft == (0, 0)
+
+        default_ui_manager.process_events(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                             {'button': 1,
+                                                              'pos': (142, 8)}))
+        # if we successfully clicked on the moved text box
+        # scroll bar then this button should be True
+        assert text_box.scroll_bar.top_button.held is True
+
+    def test_update_with_scrollbar(self, _init_pygame: None,
+                                   default_ui_manager: UIManager,
+                                   _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        text_box.scroll_bar.has_moved_recently = True
+        text_box.update(5.0)
+        assert text_box.image is not None
+
+        text_box.set_dimensions((0, 0))
+        text_box.update(0.02)
+
+        text_box.set_dimensions((150, 200))
+        text_box.update(0.02)
+        text_box.update(0.02)
+        text_box.update(0.2)
+
+        # trigger rebuild from update
+        text_box.should_trigger_full_rebuild = True
+        text_box.full_rebuild_countdown = 0.0
+        text_box.update(0.01)
+        assert text_box.image is not None
+
+    def test_update_without_scrollbar(self, _init_pygame: None,
+                                      default_ui_manager: UIManager,
+                                      _display_surface_return_none: None):
+        text_box = UITextEntryBox(initial_text='<a href=None>lalaLAlalala</a>',
+                                  relative_rect=pygame.Rect(0, 0, 150, 100),
+                                  manager=default_ui_manager)
+        default_ui_manager.mouse_position = (20, 15)
+        text_box.update(5.0)
+        default_ui_manager.mouse_position = (200, 200)
+        text_box.update(5.0)
+
+        assert text_box.image is not None
+
+    def test_redraw_from_text_block_with_scrollbar(self, _init_pygame: None,
+                                                   default_ui_manager: UIManager,
+                                                   _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        text_box.redraw_from_text_block()
+
+        assert text_box.image is not None
+
+        text_box.rect.width = 0
+        text_box.redraw_from_text_block()  # should return
+
+        assert text_box.image is not None
+
+    def test_redraw_from_text_block_no_scrollbar(self, _init_pygame: None,
+                                                 default_ui_manager: UIManager,
+                                                 _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        text_box.redraw_from_text_block()
+        assert text_box.image is not None
+
+    def test_redraw_from_chunks_with_scrollbar(self, _init_pygame: None,
+                                               default_ui_manager: UIManager,
+                                               _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        text_box.redraw_from_chunks()
+        assert text_box.image is not None
+
+    def test_full_redraw_with_scrollbar(self, _init_pygame: None,
+                                        default_ui_manager: UIManager,
+                                        _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        text_box.full_redraw()
+        assert text_box.image is not None
+
+    def test_select_with_scrollbar(self, _init_pygame: None,
+                                   default_ui_manager: UIManager,
+                                   _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        default_ui_manager.set_focus_set(text_box.get_focus_set())
+        assert text_box.scroll_bar.is_focused is True
+
+    def test_set_active_effect_fade_in(self, _init_pygame: None, default_ui_manager: UIManager,
+                                       _display_surface_return_none: None):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        text_box.set_active_effect(pygame_gui.TEXT_EFFECT_FADE_IN)
+        text_box.update(5.0)
+        assert type(text_box.active_text_effect) == pygame_gui.core.text.FadeInEffect
+
+    def test_set_active_effect_fade_out(self, _init_pygame: None, default_ui_manager: UIManager,
+                                        _display_surface_return_none: None):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        text_box.set_active_effect(pygame_gui.TEXT_EFFECT_FADE_OUT)
+        text_box.update(5.0)
+        assert type(text_box.active_text_effect) == pygame_gui.core.text.FadeOutEffect
+
+    def test_set_active_effect_invalid(self, _init_pygame: None,
+                                       default_ui_manager: UIManager,
+                                       _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+        with pytest.warns(UserWarning, match="Unsupported effect name"):
+            text_box.set_active_effect("ghost_text")
+
+    def test_set_active_effect_none(self, _init_pygame: None,
+                                    default_ui_manager: UIManager,
+                                    _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(100, 100, 150, 100),
+                                  manager=default_ui_manager)
+
+        text_box.set_active_effect(None)
+        assert text_box.active_text_effect is None
+
+    def test_set_active_effect_with_word_split(self, _init_pygame: None,
+                                               _display_surface_return_none):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes", "ui_text_box_non_default.json"))
+        manager.preload_fonts([{"name": "fira_code", "point_size": 10, "style": "regular"},
+                               {'name': 'fira_code', 'point_size': 10, 'style': 'bold'},
+                               {"name": "fira_code", "point_size": 10, "style": "italic"},
+                               {"name": "fira_code", "point_size": 10, "style": "bold_italic"}])
+        htm_text_block_2 = UITextEntryBox(initial_text='<font face=fira_code size=2 color=#000000>'
+                                          '<b>Hey, What the heck!</b>'
+                                          '<br><br>'
+                                          'This is some <a href="test">text</a> in a different box,'
+                                          ' hooray for variety - '
+                                          'if you want then you should put a ring upon it. '
+                                          '<body bgcolor=#990000>What if we do a really long word?'
+                                          '</body> '
+                                          '<b><i>derp FALALALALALALALXALALALXALALALALAAPaaaaarp'
+                                          ' gosh</b></i></font>',
+                                          relative_rect=pygame.Rect((0, 0), (250, 200)),
+                                          manager=manager,
+                                          object_id="#text_box_2")
+        htm_text_block_2.set_active_effect(pygame_gui.TEXT_EFFECT_TYPING_APPEAR)
+        htm_text_block_2.active_text_effect.text_changed = True
+        htm_text_block_2.update(5.0)
+        htm_text_block_2.update(5.0)
+        assert type(htm_text_block_2.active_text_effect) == pygame_gui.core.text.TypingAppearEffect
+
+    def test_process_event_mouse_buttons_with_scrollbar(self, _init_pygame: None,
+                                                        default_ui_manager: UIManager,
+                                                        _display_surface_return_none: None):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(0, 0, 150, 100),
+                                  manager=default_ui_manager)
+
+        processed_down_event = text_box.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                         {'button': 1, 'pos': (30, 15)}))
+        text_box.process_event(pygame.event.Event(pygame.MOUSEBUTTONUP, {'button': 1, 'pos': (80, 15)}))
+
+        assert processed_down_event is True
+
+    def test_process_event_mouse_buttons_no_scrollbar(self, _init_pygame: None,
+                                                      default_ui_manager: UIManager,
+                                                      _display_surface_return_none: None):
+        text_box = UITextEntryBox(initial_text='<a href=none>alalaadads<a/>',
+                                  relative_rect=pygame.Rect(0, 0, 150, 100),
+                                  manager=default_ui_manager)
+
+        processed_down_event = text_box.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                         {'button': 1, 'pos': (20, 15)}))
+        text_box.process_event(pygame.event.Event(pygame.MOUSEBUTTONUP, {'button': 1, 'pos': (20, 15)}))
+
+        assert processed_down_event is True
+
+    def test_rebuild(self, _init_pygame,
+                     default_ui_manager: UIManager,
+                     _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='hello',
+                                  relative_rect=pygame.Rect(0, 0, 150, 100),
+                                  manager=default_ui_manager)
+
+        text_box.rebuild()
+
+        assert text_box.image is not None
+
+        # try with 0 height rect, should return
+        text_box.rect.height = 0
+        text_box.rebuild()
+
+        assert text_box.image is not None
+
+    def test_rebuild_from_theme_data_non_default(self, _init_pygame,
+                                                 _display_surface_return_none):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes", "ui_text_box_non_default.json"))
+
+        manager.preload_fonts([{"name": "fira_code", "size:": 14, "style": "bold"},
+                               {"name": "fira_code", "size:": 14, "style": "italic"}])
+        text_box = UITextEntryBox(initial_text="<font color=#FF0000 face=fira_code>Some "
+                                               "<font color=regular_text>text</font> "
+                                               "in a <b>bold box</b> <a>using</a> "
+                                               "colours and <i>styles</i>.</font>",
+                                  relative_rect=pygame.Rect(100, 100, 200, 300),
+                                  manager=manager)
+        text_box.redraw_from_chunks()
+        text_box.full_redraw()
+        assert text_box.image is not None
+
+    @pytest.mark.filterwarnings("ignore:Invalid value")
+    @pytest.mark.filterwarnings("ignore:Colour hex code")
+    def test_rebuild_from_theme_data_bad_values(self, _init_pygame,
+                                                _display_surface_return_none):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes", "ui_text_box_bad_values.json"))
+
+        manager.preload_fonts([{"name": "fira_code", "size:": 14, "style": "bold"},
+                               {"name": "fira_code", "size:": 14, "style": "italic"}])
+        text_box = UITextEntryBox(
+            initial_text="<font color=#FF0000 face=fira_code>Some text in a <b>bold box</b> using "
+                         "colours and <i>styles</i>.</font>",
+            relative_rect=pygame.Rect(100, 100, 200, 300),
+            manager=manager)
+        assert text_box.image is not None
+
+    def test_set_dimensions(self, _init_pygame, default_ui_manager,
+                            _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al'
+                                               'al alalalal lfed alal alal alal al'
+                                               'ala lalalal lasda lal a lalalal slapl'
+                                               'alalala lal la blop lal alal aferlal al',
+                                  relative_rect=pygame.Rect(0, 0, 150, 100),
+                                  manager=default_ui_manager)
+
+        text_box.set_dimensions((200, 80))
+
+        # try to click on the slider
+        default_ui_manager.process_events(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                             {'button': 1,
+                                                              'pos': (195, 75)}))
+        # if we successfully clicked on the moved slider then this button should be True
+        assert text_box.scroll_bar.bottom_button.held is True
+
+    def test_disable(self, _init_pygame: None, default_ui_manager: UIManager,
+                     _display_surface_return_none: None):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(0, 0, 150, 100),
+                                  manager=default_ui_manager)
+
+        text_box.disable()
+
+        assert text_box.is_enabled is False
+        assert text_box.scroll_bar.is_enabled is False
+
+        # process a mouse button down event
+        text_box.scroll_bar.bottom_button.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                               {'button': 1, 'pos': text_box.scroll_bar.bottom_button.rect.center}))
+
+        text_box.scroll_bar.update(0.1)
+
+        # process a mouse button up event
+        text_box.scroll_bar.bottom_button.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONUP,
+                               {'button': 1, 'pos': text_box.scroll_bar.bottom_button.rect.center}))
+
+        assert text_box.scroll_bar.scroll_position == 0.0
+
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.focus()
+        text_entry.disable()
+
+        assert text_entry.is_enabled is False
+        # process a mouse button down event
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_d,
+                                                                           'mod': 0,
+                                                                           'unicode': 'd'}))
+
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_a, 'mod': 0,
+                                                                     'unicode': 'a'}))
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_n, 'mod': 0,
+                                                                     'unicode': 'n'}))
+
+        assert processed_key_event is False and text_entry.get_text() == ''
+
+    def test_enable(self, _init_pygame: None, default_ui_manager: UIManager,
+                    _display_surface_return_none: None):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al',
+                                  relative_rect=pygame.Rect(0, 0, 150, 100),
+                                  manager=default_ui_manager)
+
+        text_box.disable()
+        text_box.enable()
+
+        assert text_box.is_enabled is True
+        assert text_box.scroll_bar.is_enabled is True
+
+        # process a mouse button down event
+        text_box.scroll_bar.bottom_button.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                               {'button': 1, 'pos': text_box.scroll_bar.bottom_button.rect.center}))
+
+        text_box.scroll_bar.update(0.1)
+
+        # process a mouse button up event
+        text_box.scroll_bar.bottom_button.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONUP,
+                               {'button': 1, 'pos': text_box.scroll_bar.bottom_button.rect.center}))
+
+        assert text_box.scroll_bar.scroll_position != 0.0
+
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.disable()
+
+        text_entry.focus()
+        text_entry.enable()
+
+        assert text_entry.is_enabled is True
+        text_entry.focus()
+        # process a mouse button down event
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_d,
+                                                                           'mod': 0,
+                                                                           'unicode': 'd'}))
+
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_a, 'mod': 0,
+                                                                     'unicode': 'a'}))
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_n, 'mod': 0,
+                                                                     'unicode': 'n'}))
+
+        assert processed_key_event is True and text_entry.get_text() == 'dan'
+
+    def test_on_locale_changed(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               'alalalalal lalal alalalal al'
+                                               'al alalalal lfed alal alal alal al'
+                                               'ala lalalal lasda lal a lalalal slapl'
+                                               'alalala lal la blop lal alal aferlal al',
+                                  relative_rect=pygame.Rect(0, 0, 150, 100),
+                                  manager=default_ui_manager)
+
+        default_ui_manager.set_locale('fr')
+
+        default_ui_manager.set_locale('ja')
+
+        assert text_box.image is not None
+
+    def test_text_owner_interface(self, _init_pygame, default_ui_manager,
+                                  _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
+                                               'LLALAALALA ALALA ALAL ALA'
+                                               'LAALA ALALA ALALA AAaal aa'
+                                               'ALALAa laalal alalal alala'
+                                               'alalalala alalalalalal alal'
+                                               'alalalala <a href=none>alala<a/> '
+                                               'alalala ala'
+                                               '<a href=none>alalalalal lalal alalalal al'
+                                               'al alalalal lfed alal alal alal al'
+                                               'ala lalalal lasda lal a lalalal slapl'
+                                               'alalala lal la blop lal alal aferlal al</a>',
+                                  relative_rect=pygame.Rect(0, 0, 150, 100),
+                                  manager=default_ui_manager)
+
+        # these do nothing right now for full block effects
+        text_box.set_text_offset_pos((0, 0))
+        text_box.set_text_rotation(0)
+        text_box.set_text_scale(0)
+
+        assert text_box.image is not None
+
+    def test_pre_parsing(self, _init_pygame: None,
+                         default_ui_manager: UIManager,
+                         _display_surface_return_none):
+        text_box = UITextEntryBox(initial_text="Some text\n"
+                                               "On different lines with backslash n\n"
+                                               "Done.",
+                                  relative_rect=pygame.Rect(100, 100, 500, 300),
+                                  manager=default_ui_manager)
+        assert text_box.image is not None
+        assert len(text_box.text_box_layout.layout_rows) == 3
+        
+    def test_rebuild_select_area_1(self, _init_pygame, default_ui_manager,
+                                   _display_surface_return_none):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text("GOLD")
+        text_entry.select_range = [0, 2]
+        text_entry.rebuild()
+
+        assert text_entry.image is not None
+
+    def test_set_text_rebuild_select_area_2(self, _init_pygame,
+                                            _display_surface_return_none):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_non_default.json"))
+
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+
+        text_entry.set_text("GOLDEN GOD")
+        text_entry.select_range = [4, 7]
+        text_entry.rebuild()
+
+        assert text_entry.image is not None
+
+    def test_set_text_rebuild_select_area_3(self, _init_pygame):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_non_default_2.json"))
+
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+
+        text_entry.set_text("GOLDEN GOD")
+        text_entry.select_range = [4, 7]
+        text_entry.rebuild()
+
+        assert text_entry.image is not None
+
+    @pytest.mark.filterwarnings("ignore:Invalid value")
+    @pytest.mark.filterwarnings("ignore:Colour hex code")
+    def test_set_text_rebuild_select_area_3(self, _init_pygame):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_bad_values.json"))
+
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+
+        text_entry.set_text("GOLD")
+        text_entry.select_range = [0, 2]
+        text_entry.rebuild()
+
+        assert text_entry.image is not None
+
+    def test_focus(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.focus()
+
+        assert text_entry.image is not None
+
+    def test_unfocus(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.unfocus()
+
+        assert text_entry.image is not None
+
+    def test_process_event_text_entered_success(self, _init_pygame: None,
+                                                default_ui_manager: UIManager,
+                                                _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.focus()
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_d,
+                                                                           'mod': 0,
+                                                                           'unicode': 'd'}))
+
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_a, 'mod': 0,
+                                                                     'unicode': 'a'}))
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_n, 'mod': 0,
+                                                                     'unicode': 'n'}))
+
+        assert processed_key_event and text_entry.get_text() == 'dan'
+
+    def test_process_event_text_entered_with_select_range(self, _init_pygame: None,
+                                                          default_ui_manager: UIManager,
+                                                          _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('Hours and hours of fun writing tests')
+        text_entry.focus()
+        text_entry.select_range = [1, 9]
+
+        # process a mouse button down event
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_o,
+                                                                           'mod': 0,
+                                                                           'unicode': 'o'}))
+
+        assert (processed_key_event is True and
+                text_entry.get_text() == 'Ho hours of fun writing tests')
+
+    def test_process_event_text_ctrl_c(self, _init_pygame: None,
+                                       _display_surface_return_none: None):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_non_default_2.json"))
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [0, 3]
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_c,
+                                                                           'mod': pygame.KMOD_CTRL,
+                                                                           'unicode': 'c'}))
+        text_entry.cursor_on = True
+
+        assert processed_key_event and clipboard_paste() == 'dan'
+
+    def test_process_event_text_ctrl_v(self, _init_pygame: None, default_ui_manager: UIManager,
+                                       _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [1, 3]
+
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_c,
+                                                                     'mod': pygame.KMOD_CTRL,
+                                                                     'unicode': 'c'}))
+        text_entry.select_range = [0, 0]
+        text_entry.edit_position = 3
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_v,
+                                                                           'mod': pygame.KMOD_CTRL,
+                                                                           'unicode': 'v'}))
+
+        assert processed_key_event and text_entry.get_text() == 'danan'
+
+    def test_process_event_text_ctrl_v_nothing(self, _init_pygame: None,
+                                               default_ui_manager: UIManager,
+                                               _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        clipboard_copy('')
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [0, 0]
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_v,
+                                                                           'mod': pygame.KMOD_CTRL,
+                                                                           'unicode': 'v'}))
+
+        assert processed_key_event and text_entry.get_text() == 'dan'
+
+    def test_process_event_ctrl_v_at_limit(self, _init_pygame: None, default_ui_manager: UIManager,
+                                           _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.length_limit = 3
+        text_entry.select_range = [0, 3]
+
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_c,
+                                                                     'mod': pygame.KMOD_CTRL,
+                                                                     'unicode': 'c'}))
+
+        text_entry.set_text('bob')
+        text_entry.focus()
+        text_entry.select_range = [0, 3]
+        text_entry.edit_position = 0
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_v,
+                                                                           'mod': pygame.KMOD_CTRL,
+                                                                           'unicode': 'v'}))
+
+        assert processed_key_event and text_entry.get_text() == 'dan'
+
+    def test_process_event_text_ctrl_v_select_range(self, _init_pygame: None,
+                                                    default_ui_manager: UIManager,
+                                                    _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [1, 3]
+
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_c,
+                                                                     'mod': pygame.KMOD_CTRL,
+                                                                     'unicode': 'c'}))
+        text_entry.select_range = [0, 3]
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_v,
+                                                                           'mod': pygame.KMOD_CTRL,
+                                                                           'unicode': 'v'}))
+
+        assert processed_key_event
+
+        if not platform.system().upper() == "LINUX":
+            # copy and paste is unreliable on linux, this part of the test fails fairly regularly there
+            assert text_entry.get_text() == 'an'
+
+    def test_process_event_text_ctrl_a(self, _init_pygame: None, default_ui_manager: UIManager,
+                                       _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_a,
+                                                                           'mod': pygame.KMOD_CTRL,
+                                                                           'unicode': 'a'}))
+
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_c,
+                                                                     'mod': pygame.KMOD_CTRL,
+                                                                     'unicode': 'c'}))
+
+        text_entry.select_range = [0, 0]
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_v,
+                                                                     'mod': pygame.KMOD_CTRL,
+                                                                     'unicode': 'v'}))
+
+        assert processed_key_event and text_entry.get_text() == 'dandan'
+
+    def test_process_event_text_ctrl_x(self, _init_pygame: None, default_ui_manager: UIManager,
+                                       _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_a,
+                                                                           'mod': pygame.KMOD_CTRL,
+                                                                           'unicode': 'a'}))
+
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_x,
+                                                                     'mod': pygame.KMOD_CTRL,
+                                                                     'unicode': 'x'}))
+
+        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_v,
+                                                                     'mod': pygame.KMOD_CTRL,
+                                                                     'unicode': 'v'}))
+
+        assert processed_key_event and clipboard_paste() == 'dan'
+
+    def test_process_event_mouse_buttons(self, _init_pygame: None, default_ui_manager: UIManager,
+                                         _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(0, 0, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan is amazing')
+        processed_down_event = text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                           {'button': 1,
+                                                                            'pos': (30, 15)}))
+        processed_up_event = text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONUP,
+                                                                         {'button': 1,
+                                                                          'pos': (80, 15)}))
+
+        assert processed_down_event
+        assert processed_up_event
+        assert text_entry.select_range == [3, 9]
+
+    def test_process_event_mouse_button_double_click(self, _init_pygame: None,
+                                                     default_ui_manager: UIManager,
+                                                     _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(0, 0, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan is amazing')
+        processed_down_event = text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                           {'button': 1,
+                                                                            'pos': (90, 15)}))
+        processed_up_event = text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                         {'button': 1,
+                                                                          'pos': (90, 15)}))
+
+        assert (processed_down_event and processed_up_event and text_entry.select_range == [7, 14])
+
+        text_entry.set_text('')
+        processed_down_event = text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                           {'button': 1,
+                                                                            'pos': (90, 15)}))
+        processed_up_event = text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                         {'button': 1,
+                                                                          'pos': (90, 15)}))
+
+        assert (processed_down_event and processed_up_event and text_entry.select_range == [0, 0])
+
+    def test_process_event_mouse_button_double_click_in_empty_space(
+            self, _init_pygame: None,
+            default_ui_manager: UIManager,
+            _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(0, 0, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('                      dan')
+        processed_down_event = text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                           {'button': 1,
+                                                                            'pos': (90, 15)}))
+        processed_up_event = text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                         {'button': 1,
+                                                                          'pos': (90, 15)}))
+
+        assert (processed_down_event and processed_up_event and text_entry.select_range == [0, 0])
+
+    def test_process_event_mouse_button_double_click_first_word(self, _init_pygame: None,
+                                                                default_ui_manager: UIManager,
+                                                                _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(0, 0, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan is amazing')
+        processed_down_event = text_entry.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {'button': pygame.BUTTON_LEFT,
+                                                        'pos': (15, 15)}))
+        processed_up_event = text_entry.process_event(
+            pygame.event.Event(pygame.MOUSEBUTTONDOWN, {'button': pygame.BUTTON_LEFT,
+                                                        'pos': (15, 15)}))
+
+        assert (processed_down_event and processed_up_event and text_entry.select_range == [0, 3])
+
+    def test_process_event_mouse_button_up_outside(self, _init_pygame: None,
+                                                   default_ui_manager: UIManager,
+                                                   _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(0, 0, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan is amazing')
+        processed_down_event = text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
+                                                                           {'button': 1,
+                                                                            'pos': (30, 15)}))
+        text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONUP, {'button': 1,
+                                                                           'pos': (80, 50)}))
+
+        assert processed_down_event and text_entry.selection_in_progress is False
+
+    def test_process_event_text_return(self, _init_pygame: None, default_ui_manager: UIManager,
+                                       _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_RETURN}))
+
+        assert processed_key_event
+
+    def test_process_event_text_right(self, _init_pygame: None, default_ui_manager: UIManager,
+                                      _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_RIGHT}))
+
+        assert processed_key_event
+
+    def test_process_event_text_right_actually_move(self, _init_pygame: None,
+                                                    default_ui_manager: UIManager,
+                                                    _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.edit_position = 2
+        text_entry.focus()
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_RIGHT}))
+
+        assert processed_key_event
+
+    def test_process_event_text_left(self, _init_pygame: None, default_ui_manager: UIManager,
+                                     _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        assert text_entry.edit_position == 3
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_LEFT}))
+
+        assert processed_key_event
+        assert text_entry.edit_position == 2
+
+    def test_process_event_home(self, _init_pygame: None, default_ui_manager: UIManager,
+                                _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [0, 2]
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_HOME}))
+
+        assert processed_key_event
+        assert text_entry.select_range == [0, 0]
+        assert text_entry.edit_position == 0
+
+    def test_process_event_end(self, _init_pygame: None, default_ui_manager: UIManager,
+                               _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [0, 2]
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_END}))
+
+        assert processed_key_event
+        assert text_entry.select_range == [0, 0]
+        assert text_entry.edit_position == 3
+
+    def test_process_event_text_right_select_range(self, _init_pygame: None,
+                                                   default_ui_manager: UIManager,
+                                                   _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [0, 2]
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_RIGHT}))
+
+        assert processed_key_event
+
+    def test_process_event_text_left_select_range(self, _init_pygame: None,
+                                                  default_ui_manager: UIManager,
+                                                  _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [0, 2]
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_LEFT}))
+
+        assert processed_key_event
+
+    def test_process_event_delete_select_range(self, _init_pygame: None,
+                                               default_ui_manager: UIManager,
+                                               _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [0, 2]
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_DELETE}))
+
+        assert processed_key_event
+
+    def test_process_event_delete(self, _init_pygame: None, default_ui_manager: UIManager,
+                                  _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.edit_position = 1
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_DELETE}))
+
+        assert processed_key_event
+
+    def test_process_event_backspace(self, _init_pygame: None, default_ui_manager: UIManager,
+                                     _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.edit_position = 2
+        text_entry.start_text_offset = 1
+
+        processed_key_event = text_entry.process_event(
+            pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_BACKSPACE}))
+
+        assert processed_key_event
+
+    def test_process_event_backspace_select_range(self, _init_pygame: None,
+                                                  default_ui_manager: UIManager,
+                                                  _display_surface_return_none: None):
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=default_ui_manager)
+
+        text_entry.set_text('dan')
+        text_entry.focus()
+        text_entry.select_range = [1, 3]
+
+        processed_key_event = text_entry.process_event(
+            pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_BACKSPACE}))
+
+        assert processed_key_event
+
+    @pytest.mark.filterwarnings("ignore:Invalid value")
+    @pytest.mark.filterwarnings("ignore:Colour hex code")
+    def test_redraw_selected_text(self, _init_pygame):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_bad_values.json"))
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+
+        text_entry.set_text("Yellow su")
+        text_entry.select_range = [3, 8]
+        text_entry.start_text_offset = 500
+
+    def test_redraw_selected_text_different_theme(self, _init_pygame):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_non_default_2.json"))
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+
+        text_entry.set_text("Yellow su")
+        text_entry.select_range = [3, 9]
+
+    def test_update(self,  _init_pygame):
+        pygame.display.init()
+
+        class MouselessManager(UIManager):
+            fixed_mouse_pos = (0, 0)
+
+            def _update_mouse_position(self):
+                self.mouse_position = MouselessManager.fixed_mouse_pos
+
+        manager = MouselessManager((800, 600), os.path.join("tests", "data",
+                                                            "themes",
+                                                            "ui_text_entry_line_non_default.json"))
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+
+        text_entry.update(0.01)
+
+        assert text_entry.alive()
+        assert not manager.text_input_hovered
+
+        MouselessManager.fixed_mouse_pos = (200, 115)
+        manager.update(0.01)
+        assert manager.text_input_hovered
+
+        text_entry.kill()
+
+        text_entry.update(0.01)
+
+        assert not text_entry.alive()
+
+    def test_update_after_click(self,  _init_pygame, _display_surface_return_none: None, default_ui_manager):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_non_default.json"))
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(0, 0, 200, 30),
+                                    manager=manager)
+
+        text_entry.set_text('Wow testing is great so amazing')
+        text_entry.focus()
+
+        text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN, {'button': 1,
+                                                                             'pos': (30, 15)}))
+        default_ui_manager.mouse_position = (70, 15)
+
+        text_entry.update(0.01)
+
+    def test_update_newline_after_click(self,  _init_pygame, _display_surface_return_none: None, default_ui_manager):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_non_default.json"))
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(0, 0, 200, 30),
+                                    manager=manager)
+
+        text_entry.set_text('Wow testing is great so amazing\n\n')
+        text_entry.focus()
+
+        text_entry.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN, {'button': 1,
+                                                                             'pos': (30, 15)}))
+        default_ui_manager.mouse_position = (70, 15)
+
+        text_entry.update(0.01)
+
+    def test_update_after_long_wait(self,  _init_pygame):
+        pygame.display.init()
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_non_default.json"))
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+
+        text_entry.update(0.01)
+        text_entry.update(5.0)
+
+    def test_update_cursor_blink(self,  _init_pygame, _display_surface_return_none: None):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_non_default.json"))
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+
+        text_entry.focus()
+        text_entry.cursor_blink_delay_after_moving_acc = 10.0
+        text_entry.update(0.01)
+        text_entry.blink_cursor_time_acc = 10.0
+        text_entry.update(0.01)
+        text_entry.blink_cursor_time_acc = 10.0
+        text_entry.update(0.01)
+
+    @pytest.mark.filterwarnings("ignore:Invalid value")
+    @pytest.mark.filterwarnings("ignore:Colour hex code")
+    def test_rebuild_from_theme_data_bad_values(self, _init_pygame):
+        manager = UIManager((800, 600), os.path.join("tests", "data",
+                                                     "themes",
+                                                     "ui_text_entry_line_bad_values.json"))
+
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                    manager=manager)
+        assert text_entry.image is not None
+
+    def test_set_position(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        test_container = UIContainer(relative_rect=pygame.Rect(100, 100, 300, 60),
+                                     manager=default_ui_manager)
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(0, 0, 150, 30),
+                                    container=test_container,
+                                    manager=default_ui_manager)
+
+        text_entry.set_position((150.0, 30.0))
+
+        assert (text_entry.relative_rect.topleft == (50, -70) and
+                text_entry.drawable_shape.containing_rect.topleft == (150, 30))
+
+    def test_set_relative_position(self, _init_pygame, default_ui_manager,
+                                   _display_surface_return_none):
+        test_container = UIContainer(relative_rect=pygame.Rect(50, 50, 300, 250),
+                                     manager=default_ui_manager)
+        text_entry = UITextEntryBox(relative_rect=pygame.Rect(0, 0, 150, 30),
+                                    container=test_container,
+                                    manager=default_ui_manager)
+
+        text_entry.set_relative_position((50.0, 30.0))
+
+        assert text_entry.rect.topleft == (100, 80)
+
+
+if __name__ == '__main__':
+    pytest.console_main()
diff --git a/tests/test_elements/test_ui_text_entry_line.py b/tests/test_elements/test_ui_text_entry_line.py
index 21ef484e..896b84a5 100644
--- a/tests/test_elements/test_ui_text_entry_line.py
+++ b/tests/test_elements/test_ui_text_entry_line.py
@@ -922,9 +922,16 @@ def test_redraw_selected_text_different_them(self, _init_pygame):
 
     def test_update(self,  _init_pygame):
         pygame.display.init()
-        manager = UIManager((800, 600), os.path.join("tests", "data",
-                                                     "themes",
-                                                     "ui_text_entry_line_non_default.json"))
+
+        class MouselessManager(UIManager):
+            fixed_mouse_pos = (0, 0)
+
+            def _update_mouse_position(self):
+                self.mouse_position = MouselessManager.fixed_mouse_pos
+
+        manager = MouselessManager((800, 600), os.path.join("tests", "data",
+                                                            "themes",
+                                                            "ui_text_entry_line_non_default.json"))
         text_entry = UITextEntryLine(relative_rect=pygame.Rect(100, 100, 200, 30),
                                      manager=manager)
 
@@ -933,8 +940,8 @@ def test_update(self,  _init_pygame):
         assert text_entry.alive()
         assert not manager.text_input_hovered
 
-        manager.mouse_position = (150, 115)
-        text_entry.update(0.01)
+        MouselessManager.fixed_mouse_pos = (200, 115)
+        manager.update(0.01)
         assert manager.text_input_hovered
 
         text_entry.kill()
diff --git a/tests/test_elements/test_ui_window.py b/tests/test_elements/test_ui_window.py
index a772eb45..f0a9e43f 100644
--- a/tests/test_elements/test_ui_window.py
+++ b/tests/test_elements/test_ui_window.py
@@ -299,16 +299,16 @@ def test_get_top_layer(self, _init_pygame, default_ui_manager,
                        manager=default_ui_manager,
                        container=window)
 
-        assert window.get_top_layer() == 4
+        assert window.get_top_layer() == 5
         window.update(0.05)
-        assert window.get_top_layer() == 5  # This used to be 6, maybe it should be - drop downs?
+        assert window.get_top_layer() == 7  # This used to be 6, maybe it should be - drop downs?
 
     def test_change_layer(self, _init_pygame, default_ui_manager,
                           _display_surface_return_none):
         window = UIWindow(pygame.Rect(0, 0, 200, 200), window_display_title="Test Window",
                           manager=default_ui_manager)
 
-        assert window.get_top_layer() == 4
+        assert window.get_top_layer() == 5
 
         window.change_layer(10)
 
diff --git a/tests/test_ui_manager.py b/tests/test_ui_manager.py
index 4dd2c7d7..81512e0b 100644
--- a/tests/test_ui_manager.py
+++ b/tests/test_ui_manager.py
@@ -8,6 +8,7 @@
 from pygame_gui.ui_manager import UIManager
 from pygame_gui.core import UIAppearanceTheme, UIWindowStack
 from pygame_gui.elements.ui_button import UIButton
+from pygame_gui.elements.ui_text_entry_line import UITextEntryLine
 from pygame_gui.elements.ui_text_box import UITextBox
 from pygame_gui.windows.ui_message_window import UIMessageWindow
 from pygame_gui.elements.ui_window import UIWindow
@@ -340,9 +341,12 @@ def test_set_active_cursor(self, _init_pygame, _display_surface_return_none):
         assert manager._active_cursor == pygame.cursors.Cursor(pygame.SYSTEM_CURSOR_HAND)
 
     def test_set_hovering_text(self, _init_pygame, _display_surface_return_none):
-        manager = UIManager((800, 600))
+        class MouselessManager(UIManager):
+            def _update_mouse_position(self):
+                self.mouse_position = (400, 15)
+        manager = MouselessManager((800, 600))
 
-        manager.set_text_input_hovered(True)
+        text_entry = UITextEntryLine(pygame.Rect(0, 0, 800, 30), manager=manager)
 
         manager.update(0.5)
         assert manager._active_cursor == pygame.cursors.Cursor(pygame.SYSTEM_CURSOR_IBEAM)