Repository: MyreMylar/pygame_gui (language: python)

Issue:
Additional Theming Parameters for UITextEntryBox
**Is your feature request related to a problem? Please describe.**
UITextEntryBox doesn't seem to have the same theming parameters as the UITextEntryLine, such as "selected_text", "selected_bg", and "text_cursor".

**Describe the solution you'd like**
I'd like for those parameters to be added to UITextEntryBox.




Discussion / hints:
I will add it to the to do list.

Cheers for the report.

Fix patch:
diff --git a/docs/source/theme_reference/theme_text_box.rst b/docs/source/theme_reference/theme_text_box.rst
index 6da5ec13..67318e20 100644
--- a/docs/source/theme_reference/theme_text_box.rst
+++ b/docs/source/theme_reference/theme_text_box.rst
@@ -25,7 +25,9 @@ Colours
 also be a colour gradient:
 
  - "**dark_bg**" -  The background colour/gradient of the text box element.
+ - "**selected_bg**" - The colour/gradient the background changes to when the text above it is selected.
  - "**normal_border**" - The colour/gradient of the border around the text box element.
+ - "**selected_text**" - The colour/gradient of text when it has been selected.
  - "**link_text**" - The default colour/gradient of any links in the text.
  - "**link_hover**" - The colour/gradient of link text when we hover over it with the mouse.
  - "**link_selected**" - The colour/gradient of link text when they are clicked on with the mouse.
diff --git a/docs/source/theme_reference/theme_text_entry_box.rst b/docs/source/theme_reference/theme_text_entry_box.rst
index b8b61591..b2bcd21f 100644
--- a/docs/source/theme_reference/theme_text_entry_box.rst
+++ b/docs/source/theme_reference/theme_text_entry_box.rst
@@ -25,10 +25,13 @@ Colours
 also be a colour gradient:
 
  - "**dark_bg**" -  The background colour/gradient of the text box element.
+ - "**selected_bg**" - The colour/gradient the background changes to when the text above it is selected.
  - "**normal_border**" - The colour/gradient of the border around the text box element.
+ - "**selected_text**" - The colour/gradient of text when it has been selected.
  - "**link_text**" - The default colour/gradient of any links in the text.
  - "**link_hover**" - The colour/gradient of link text when we hover over it with the mouse.
  - "**link_selected**" - The colour/gradient of link text when they are clicked on with the mouse.
+ - "**text_cursor**" - The colour of the text cursor.
 
 Misc
 ----
diff --git a/pygame_gui/core/drawable_shapes/drawable_shape.py b/pygame_gui/core/drawable_shapes/drawable_shape.py
index e4f6fff7..75c0bcea 100644
--- a/pygame_gui/core/drawable_shapes/drawable_shape.py
+++ b/pygame_gui/core/drawable_shapes/drawable_shape.py
@@ -595,6 +595,8 @@ def build_text_layout(self):
                                                  editable=self.editable_text)
             if 'selected_bg' in self.theming:
                 self.text_box_layout.selection_colour = self.theming['selected_bg']
+            if 'selected_text' in self.theming:
+                self.text_box_layout.selection_text_colour = self.theming['selected_text']
             if 'text_cursor_colour' in self.theming:
                 self.text_box_layout.set_cursor_colour(self.theming['text_cursor_colour'])
             self.align_all_text_rows()
diff --git a/pygame_gui/core/gui_font_freetype.py b/pygame_gui/core/gui_font_freetype.py
index a452f935..ffb5d7c8 100644
--- a/pygame_gui/core/gui_font_freetype.py
+++ b/pygame_gui/core/gui_font_freetype.py
@@ -31,6 +31,8 @@ def __init__(self, file: Optional[FileArg], size: Union[int, float],
                 self.__internal_font.strong = style['bold']
                 self.__internal_font.oblique = style['italic']
 
+    def size(self, text: str) -> Tuple[int, int]:
+        return self.get_rect(text).size
 
     @property
     def underline(self) -> bool:
diff --git a/pygame_gui/core/gui_font_pygame.py b/pygame_gui/core/gui_font_pygame.py
index 0bae08b4..bbed5df5 100644
--- a/pygame_gui/core/gui_font_pygame.py
+++ b/pygame_gui/core/gui_font_pygame.py
@@ -42,7 +42,7 @@ def __init__(self, file: Optional[FileArg], size: Union[int, float],
                 self.__internal_font.bold = style['bold']
                 self.__internal_font.italic = style['italic']
 
-    def size(self, text: str):
+    def size(self, text: str) -> Tuple[int, int]:
         return self.get_rect(text).size
 
     @property
diff --git a/pygame_gui/core/interfaces/gui_font_interface.py b/pygame_gui/core/interfaces/gui_font_interface.py
index 75889cdd..90ef822f 100644
--- a/pygame_gui/core/interfaces/gui_font_interface.py
+++ b/pygame_gui/core/interfaces/gui_font_interface.py
@@ -8,6 +8,15 @@ class IGUIFontInterface(metaclass=ABCMeta):
     A font interface, so we can easily switch between pygame.freetype.Font and pygame.Font.
     """
 
+    def size(self, text: str) -> Tuple[int, int]:
+        """
+        Return the pixel size of a given text string in this font
+
+        :param text: the text to check.
+
+        :return: the width & height in pixels.
+        """
+
     @abstractmethod
     def render_premul(self, text: str, text_color: Color) -> Surface:
         """
diff --git a/pygame_gui/core/interfaces/manager_interface.py b/pygame_gui/core/interfaces/manager_interface.py
index c499b7d4..1b672f51 100644
--- a/pygame_gui/core/interfaces/manager_interface.py
+++ b/pygame_gui/core/interfaces/manager_interface.py
@@ -273,9 +273,9 @@ def get_locale(self) -> str:
         """
 
     @abstractmethod
-    def set_text_input_hovered(self, hovering_text_input: bool):
+    def set_text_hovered(self, hovering_text_input: bool):
         """
-        Set to true when hovering an area text can be input into.
+        Set to true when hovering an area containing selectable text.
 
         Currently, switches the cursor to the I-Beam cursor.
 
diff --git a/pygame_gui/core/text/text_box_layout.py b/pygame_gui/core/text/text_box_layout.py
index 80315636..4502b8a0 100644
--- a/pygame_gui/core/text/text_box_layout.py
+++ b/pygame_gui/core/text/text_box_layout.py
@@ -85,6 +85,8 @@ def __init__(self,
         self.alpha = 255
         self.pre_alpha_final_surf = None  # only need this if we apply non-255 alpha
 
+        self.plain_text = ""
+
         self.layout_rect_queue = self.input_data_rect_queue.copy()
         current_row = TextBoxLayoutRow(row_start_x=self.layout_rect.x,
                                        row_start_y=self.layout_rect.y, row_index=0,
@@ -106,6 +108,7 @@ def __init__(self,
         self.last_horiz_cursor_row_pos = 0
 
         self.selection_colour = pygame.Color(128, 128, 200, 255)
+        self.selection_text_colour = pygame.Color(255, 255, 255, 255)
         self.selected_chunks = []
         self.selected_rows = []
         self.selection_start_index = 0
@@ -115,6 +118,16 @@ def __init__(self,
 
         self.cursor_colour = pygame.Color('#FFFFFFFF')
 
+    def _update_plain_text(self):
+        plain_text = ""
+        for row in self.layout_rows:
+            for item in row.items:
+                if isinstance(item, TextLineChunkFTFont) or isinstance(item, HyperlinkTextChunk):
+                    plain_text += item.text
+                if isinstance(item, LineBreakLayoutRect):
+                    plain_text += "\n"
+        self.plain_text = plain_text
+
     def reprocess_layout_queue(self, layout_rect):
         """
         Re-lays out already parsed text data. Useful to call if the layout requirements have
@@ -166,6 +179,8 @@ def _process_layout_queue(self, input_queue, current_row):
         if self.dynamic_height:
             self.view_rect.height = self.layout_rect.height
 
+        self._update_plain_text()
+
     def _add_row_to_layout(self, current_row: TextBoxLayoutRow, last_row=False):
         # handle an empty row being added to layout
         # otherwise we add infinite rows with no height
@@ -826,11 +841,13 @@ def set_text_selection(self, start_index, end_index):
                         if isinstance(chunk, TextLineChunkFTFont):
                             if chunk == start_chunk and chunk == end_chunk:
                                 start_selection = True
+                                end_selection = True
                                 chunk.selected_text = chunk.text[
                                                       start_letter_index:end_letter_index]  # should probably check for text rect here
                                 chunk.selection_rect = pygame.Rect((start_chunk_x_pos, 0),
                                                                    (end_chunk_x_pos - start_chunk_x_pos, chunk.height))
                                 chunk.selection_colour = self.selection_colour
+                                chunk.selection_text_colour = self.selection_text_colour
                                 chunk.selection_start_index = start_letter_index
                                 chunk.is_selected = True
                                 self.selected_chunks.append(chunk)
@@ -841,19 +858,21 @@ def set_text_selection(self, start_index, end_index):
                                 chunk.selection_rect = pygame.Rect((start_chunk_x_pos, 0),
                                                                    (chunk.width - start_chunk_x_pos, chunk.height))
                                 chunk.selection_colour = self.selection_colour
+                                chunk.selection_text_colour = self.selection_text_colour
                                 chunk.selection_start_index = start_letter_index
                                 chunk.is_selected = True
                                 self.selected_chunks.append(chunk)
                             elif chunk == end_chunk:
+                                end_selection = True
                                 if end_letter_index == 0:
                                     pass
                                 else:
-                                    end_selection = True
                                     chunk.selected_text = chunk.text[
                                                           :end_letter_index]  # should probably check for text rect here
                                     chunk.selection_rect = pygame.Rect((0, 0),
                                                                        (end_chunk_x_pos, chunk.height))
                                     chunk.selection_colour = self.selection_colour
+                                    chunk.selection_text_colour = self.selection_text_colour
                                     chunk.is_selected = True
                                     self.selected_chunks.append(chunk)
 
@@ -862,6 +881,7 @@ def set_text_selection(self, start_index, end_index):
                                 chunk.selected_text = chunk.text[:]
                                 chunk.selection_rect = pygame.Rect((0, 0), chunk.size)
                                 chunk.selection_colour = self.selection_colour
+                                chunk.selection_text_colour = self.selection_text_colour
                                 chunk.is_selected = True
                                 self.selected_chunks.append(chunk)
                         elif isinstance(chunk, LineBreakLayoutRect) or isinstance(chunk, ImageLayoutRect):
@@ -872,6 +892,8 @@ def set_text_selection(self, start_index, end_index):
                                     pass
                                 else:
                                     chunk.is_selected = True
+                                    chunk.selection_colour = self.selection_colour
+                                    chunk.selection_text_colour = self.selection_text_colour
                                     self.selected_chunks.append(chunk)
                             if chunk == end_chunk:
                                 end_selection = True
@@ -921,10 +943,7 @@ def _find_chunk_and_chunk_x(self, index: int):
                             break
 
         if found_chunk is not None and isinstance(found_chunk, TextLineChunkFTFont):
-            x_pos_in_chunk = sum([char_metric[4]
-                                  for char_metric in
-                                  found_chunk.font.get_metrics(
-                                      found_chunk.text[:letter_index]) if char_metric])
+            x_pos_in_chunk = found_chunk.font.size(found_chunk.text[:letter_index])[0]
         return found_chunk, x_pos_in_chunk, letter_index, row_index
 
     def _find_and_split_chunk(self, index: int, return_rhs: bool = False):
@@ -1128,7 +1147,6 @@ def delete_selected_text(self):
                                 if isinstance(chunk, TextLineChunkFTFont):
                                     chunk.selected_text = chunk.text[:]
                                     chunk.selection_rect = pygame.Rect((0, 0), chunk.size)
-                                    chunk.selection_colour = self.selection_colour
                                     chunk.is_selected = True
                                 elif isinstance(chunk, ImageLayoutRect) or isinstance(chunk, LineBreakLayoutRect):
                                     chunk.is_selected = True
diff --git a/pygame_gui/core/text/text_box_layout_row.py b/pygame_gui/core/text/text_box_layout_row.py
index ec221886..344d3656 100644
--- a/pygame_gui/core/text/text_box_layout_row.py
+++ b/pygame_gui/core/text/text_box_layout_row.py
@@ -415,16 +415,11 @@ def find_cursor_pos_from_click_pos(self, click_pos: Tuple[int, int], num_rows: i
                         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
-                                                  chunk.font.get_metrics(
-                                                      chunk.text[:letter_index]) if char_metric])
+                        cursor_draw_width += chunk.font.size(chunk.text[:letter_index])[0]
                         letter_acc += letter_index
                         found_chunk = True
                     else:
-                        cursor_draw_width += sum([char_metric[4]
-                                                  for char_metric in
-                                                  chunk.font.get_metrics(chunk.text) if char_metric])
+                        cursor_draw_width += chunk.font.size(chunk.text)[0]
                         letter_acc += chunk.letter_count
         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.
@@ -433,9 +428,7 @@ def find_cursor_pos_from_click_pos(self, click_pos: Tuple[int, int], num_rows: i
                 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_draw_width -= last_chunk.font.size(" ")[0]
 
         cursor_index = min(self.letter_count, max(0, letter_acc))
         return cursor_index, cursor_draw_width
@@ -459,16 +452,11 @@ def set_cursor_position(self, cursor_pos):
             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])
-
+                    cursor_draw_width += chunk.font.size(chunk.text[:chunk_letter_pos])[0]
                     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])
+                cursor_draw_width += chunk.font.size(chunk.text)[0]
             elif isinstance(chunk, LineBreakLayoutRect):
                 pass
 
diff --git a/pygame_gui/core/text/text_line_chunk.py b/pygame_gui/core/text/text_line_chunk.py
index 4230fe6e..ad453d90 100644
--- a/pygame_gui/core/text/text_line_chunk.py
+++ b/pygame_gui/core/text/text_line_chunk.py
@@ -13,6 +13,8 @@
 """
 from typing import Optional, Union, Tuple, List
 
+import pygame
+
 from pygame_gui.core.interfaces.gui_font_interface import IGUIFontInterface
 
 from pygame import Color, Surface, Rect, BLEND_PREMULTIPLIED, BLEND_RGBA_MULT, SRCALPHA
@@ -78,6 +80,7 @@ def __init__(self, text: str,
         self.is_selected = False  # True when the whole chunk is selected - or part of it.
         self.is_active = False
         self.selection_colour = Color(128, 128, 128, 255)
+        self.selection_text_colour = Color(255, 255, 255, 255)
 
         self.should_centre_from_baseline = False
 
@@ -169,11 +172,6 @@ def finalise(self,
         if len(self.text) == 0:
             return
 
-        # if self.selection_rect is not None:
-        #     bg_col: Union[Color, ColourGradient] = self.selection_colour
-        # else:
-        bg_col = self.bg_colour
-
         final_str_text = self.text if letter_end is None else self.text[:letter_end]
         # update chunk width for drawing only, need to include the text origin offset
         # to make the surface wide enough
@@ -191,19 +189,9 @@ def finalise(self,
         self.font.underline = self.underlined  # set underlined state
         if self.underlined:
             self.font.underline_adjustment = 0.5
-        if isinstance(self.colour, ColourGradient):
-            surface = self._draw_text_fg_gradient(bg_col, chunk_draw_height, chunk_draw_width,
-                                                  chunk_x_origin, final_str_text, row_bg_height,
-                                                  row_chunk_origin)
-
-        elif isinstance(bg_col, ColourGradient):
-            surface = self._draw_text_bg_gradient(bg_col, chunk_draw_height, chunk_draw_width,
-                                                  chunk_x_origin, final_str_text, row_bg_height,
-                                                  row_chunk_origin)
-        else:
-            surface = self._draw_text_no_gradient(bg_col, chunk_draw_height, chunk_draw_width,
-                                                  chunk_x_origin, final_str_text, row_bg_height,
-                                                  row_chunk_origin)
+
+        surface = self._draw_text(chunk_draw_height, chunk_draw_width,
+                                  chunk_x_origin, final_str_text, row_bg_height, row_chunk_origin)
 
         target_surface = self._finalise_horizontal_scroll(target_area,
                                                           text_shadow_width,
@@ -241,110 +229,92 @@ def _finalise_horizontal_scroll(self, target_area, text_shadow_width, x_scroll_o
                                 special_flags=BLEND_PREMULTIPLIED)
         return target_surface
 
-    def _draw_text_no_gradient(self, bg_col, chunk_draw_height, chunk_draw_width, chunk_x_origin,
-                               final_str_text, row_bg_height, row_chunk_origin):
-
-        text_surface = self.font.render_premul_to(final_str_text, self.colour,
-                                                  surf_size=(chunk_draw_width, chunk_draw_height),
-                                                  surf_position=(chunk_x_origin, row_chunk_origin))
-        # This is a hacky way to convert text to pre-multiplied alpha with a SDL2 alpha blit
-        # text_surface = Surface((chunk_draw_width, chunk_draw_height),
-        #                        flags=SRCALPHA, depth=32)
-        # temp_text_surface = text_surface.copy()
-        # self.font.render_to(temp_text_surface, (chunk_x_origin, row_chunk_origin),
-        #                     final_str_text, fgcolor=self.colour)
-        # text_surface.blit(temp_text_surface, (0, 0), special_flags=pygame.BLEND_ALPHA_SDL2)
-        surface = Surface((chunk_draw_width, row_bg_height), flags=SRCALPHA, depth=32)
-        surface.fill(bg_col)
-        self._handle_text_selection_drawing(surface)
-        # center the text in the line
-        text_rect = text_surface.get_rect()
-        if self.should_centre_from_baseline:
-            padless_origin = self.y_origin - self.font_y_padding
-            half_padless_origin = int(round(0.5 * padless_origin))
-            text_rect.y = (surface.get_rect().centery -
-                           self.font_y_padding - half_padless_origin)
-        else:
-            text_rect.centery = surface.get_rect().centery
-        # apply any shadow effects
-        self._apply_shadow_effect(surface, text_rect, final_str_text,
-                                  text_surface, (chunk_x_origin, row_chunk_origin))
-        surface.blit(text_surface, text_rect, special_flags=BLEND_PREMULTIPLIED)
-        return surface
-
-    def _handle_text_selection_drawing(self, surface):
+    def _handle_bg_selection_and_bg_drawing(self, size) -> pygame.Surface:
+        surface = Surface(size, flags=SRCALPHA, depth=32)
         if self.selection_rect is not None and (self.selection_rect.width != 0 or self.selection_rect.height != 0):
-            if isinstance(self.selection_colour, Color):
-                surface.fill(self.selection_colour, self.selection_rect)
-            elif isinstance(self.selection_colour, ColourGradient):
+            # we have a selection rect, we may have up to two rects of normal background either side of this
+            before_rect = pygame.Rect(0, 0, self.selection_rect.left, self.selection_rect.height)
+            after_rect = pygame.Rect(self.selection_rect.right, 0, surface.get_width(), self.selection_rect.height)
+            if before_rect.width > 0:
+                if isinstance(self.bg_colour, ColourGradient):
+                    surface.fill(Color('#FFFFFFFF'), before_rect)
+                    self.bg_colour.apply_gradient_to_surface(surface, before_rect)
+                elif isinstance(self.bg_colour, Color):
+                    surface.fill(self.bg_colour, before_rect)
+
+            if isinstance(self.selection_colour, ColourGradient):
                 surface.fill(Color('#FFFFFFFF'), self.selection_rect)
                 self.selection_colour.apply_gradient_to_surface(surface, self.selection_rect)
+            elif isinstance(self.selection_colour, Color):
+                surface.fill(self.selection_colour, self.selection_rect)
+
+            if after_rect.width > 0:
+                if isinstance(self.bg_colour, ColourGradient):
+                    surface.fill(Color('#FFFFFFFF'), after_rect)
+                    self.bg_colour.apply_gradient_to_surface(surface, after_rect)
+                elif isinstance(self.bg_colour, Color):
+                    surface.fill(self.bg_colour, after_rect)
 
-    def _draw_text_bg_gradient(self, bg_col, chunk_draw_height, chunk_draw_width, chunk_x_origin,
-                               final_str_text, row_bg_height, row_chunk_origin):
-        # draw the text first
-        text_surface = self.font.render_premul_to(final_str_text, self.colour,
-                                                  surf_size=(chunk_draw_width, chunk_draw_height),
-                                                  surf_position=(chunk_x_origin, row_chunk_origin))
-        # This is a hacky way to convert text to pre-multiplied alpha with a SDL2 alpha blit
-        # text_surface = Surface((chunk_draw_width, chunk_draw_height),
-        #                               flags=SRCALPHA, depth=32)
-        # temp_text_surface = text_surface.copy()
-        # self.font.render_to(temp_text_surface, (chunk_x_origin, row_chunk_origin),
-        #                     final_str_text, fgcolor=self.colour)
-        # text_surface.blit(temp_text_surface, (0, 0), special_flags=pygame.BLEND_ALPHA_SDL2)
-        # then make the background
-        surface = Surface((chunk_draw_width, row_bg_height),
-                          flags=SRCALPHA, depth=32)
-        surface.fill(Color('#FFFFFFFF'))
-        bg_col.apply_gradient_to_surface(surface)
-        self._handle_text_selection_drawing(surface)
-        # center the text in the line
-        text_rect = text_surface.get_rect()
-        if self.should_centre_from_baseline:
-            padless_origin = self.y_origin - self.font_y_padding
-            half_padless_origin = int(round(0.5 * padless_origin))
-            text_rect.y = surface.get_rect().centery - self.font_y_padding - half_padless_origin
         else:
-            text_rect.centery = surface.get_rect().centery
-        # apply any shadow effects
-        self._apply_shadow_effect(surface, text_rect, final_str_text,
-                                  text_surface, (chunk_x_origin, row_chunk_origin))
-        surface.blit(text_surface, text_rect, special_flags=BLEND_PREMULTIPLIED)
+            if isinstance(self.bg_colour, ColourGradient):
+                surface.fill(Color('#FFFFFFFF'))
+                self.bg_colour.apply_gradient_to_surface(surface)
+            else:
+                surface.fill(self.bg_colour)
+
         return surface
 
-    def _draw_text_fg_gradient(self, bg_col, chunk_draw_height, chunk_draw_width, chunk_x_origin,
-                               final_str_text, row_bg_height, row_chunk_origin):
-        # draw the text first
-        # Anti-aliasing on text is not done with pre-multiplied alpha so we need to bake
-        # this text onto a surface with a normal blit before it can enter the pre-multipled
-        # blitting pipeline. This current setup may be a bit wrong but it works OK for gradients
-        # on the normal text colour.
-        text_surface = self.font.render_premul_to(final_str_text, Color('#FFFFFFFF'),
-                                                  surf_size=(chunk_draw_width, chunk_draw_height),
-                                                  surf_position=(chunk_x_origin, row_chunk_origin))
-        # This is a hacky way to convert text to pre-multiplied alpha with a SDL2 alpha blit
-        # text_surface = Surface((chunk_draw_width, chunk_draw_height),
-        #                        flags=SRCALPHA, depth=32)
-        # temp_text_surface = text_surface.copy()
-        # self.font.render_to(temp_text_surface, (chunk_x_origin, row_chunk_origin),
-        #                     final_str_text, fgcolor=Color('#FFFFFFFF'))
-        # text_surface.blit(temp_text_surface, (0, 0), special_flags=pygame.BLEND_ALPHA_SDL2)
-        self.colour.apply_gradient_to_surface(text_surface)
-        # then make the background
-        surface = Surface((chunk_draw_width, row_bg_height), flags=SRCALPHA, depth=32)
-        if isinstance(bg_col, ColourGradient):
-            surface.fill(Color('#FFFFFFFF'))
-            bg_col.apply_gradient_to_surface(surface)
+    def _handle_text_selection_and_text_drawing(self, final_str_text, chunk_draw_width, chunk_draw_height,
+                                                chunk_x_origin, row_chunk_origin) -> pygame.Surface:
+        text_surface: pygame.Surface = self.font.render_premul_to(final_str_text, Color('#FFFFFFFF'),
+                                                                  surf_size=(chunk_draw_width, chunk_draw_height),
+                                                                  surf_position=(chunk_x_origin, row_chunk_origin))
+
+        if (self.selection_rect is not None
+                and (self.selection_rect.width != 0 or self.selection_rect.height != 0)
+                and (self.selection_text_colour != self.colour)):
+            # we have a selection rect, we may have up to two rects of normal background either side of this
+            before_rect = pygame.Rect(0, 0, self.selection_rect.left, self.selection_rect.height)
+            after_rect = pygame.Rect(self.selection_rect.right, 0, chunk_draw_width, self.selection_rect.height)
+            if before_rect.width > 0:
+                if isinstance(self.colour, ColourGradient):
+                    self.colour.apply_gradient_to_surface(text_surface, before_rect)
+                elif isinstance(self.colour, Color):
+                    text_surface.fill(self.colour, before_rect, special_flags=pygame.BLEND_RGBA_MULT)
+
+            if isinstance(self.selection_text_colour, ColourGradient):
+                self.selection_text_colour.apply_gradient_to_surface(text_surface, self.selection_rect)
+            elif isinstance(self.selection_text_colour, Color):
+                text_surface.fill(self.selection_text_colour, self.selection_rect, special_flags=pygame.BLEND_RGBA_MULT)
+
+            if after_rect.width > 0:
+                if isinstance(self.colour, ColourGradient):
+                    self.colour.apply_gradient_to_surface(text_surface, after_rect)
+                elif isinstance(self.colour, Color):
+                    text_surface.fill(self.colour, after_rect, special_flags=pygame.BLEND_RGBA_MULT)
+
         else:
-            surface.fill(bg_col)
-        self._handle_text_selection_drawing(surface)
+            if isinstance(self.colour, ColourGradient):
+                self.colour.apply_gradient_to_surface(text_surface)
+            else:
+                text_surface.fill(self.colour, special_flags=pygame.BLEND_RGBA_MULT)
+
+        return text_surface
+
+    def _draw_text(self, chunk_draw_height, chunk_draw_width, chunk_x_origin,
+                   final_str_text, row_bg_height, row_chunk_origin):
+
+        text_surface = self._handle_text_selection_and_text_drawing(final_str_text, chunk_draw_width, chunk_draw_height,
+                                                                    chunk_x_origin, row_chunk_origin)
+
+        surface = self._handle_bg_selection_and_bg_drawing((chunk_draw_width, row_bg_height))
         # center the text in the line
         text_rect = text_surface.get_rect()
         if self.should_centre_from_baseline:
             padless_origin = self.y_origin - self.font_y_padding
             half_padless_origin = int(round(0.5 * padless_origin))
-            text_rect.y = surface.get_rect().centery - self.font_y_padding - half_padless_origin
+            text_rect.y = (surface.get_rect().centery -
+                           self.font_y_padding - half_padless_origin)
         else:
             text_rect.centery = surface.get_rect().centery
         # apply any shadow effects
@@ -364,16 +334,6 @@ def _apply_shadow_effect(self, surface, text_rect, text_str, text_surface, origi
             shadow_surface = self.font.render_premul_to(text_str, shadow_colour,
                                                         surf_size=text_surface.get_size(),
                                                         surf_position=origin)
-            # This is a hacky way to convert text to
-            # pre-multiplied alpha with a SDL2 alpha blit
-            # shadow_surface = text_surface.copy()
-            # temp_shadow_surface = shadow_surface.copy()
-            #
-            # self.font.render_to(temp_shadow_surface, origin,
-            #                     text_str,
-            #                     fgcolor=shadow_colour)
-            # shadow_surface.blit(temp_shadow_surface, (0, 0),
-            #                     special_flags=pygame.BLEND_ALPHA_SDL2)
 
             for y_pos in range(-shadow_size, shadow_size + 1):
                 shadow_text_rect = Rect((text_rect.x + shadow_offset[0],
diff --git a/pygame_gui/elements/ui_text_box.py b/pygame_gui/elements/ui_text_box.py
index 5a6547cb..9d9432e6 100644
--- a/pygame_gui/elements/ui_text_box.py
+++ b/pygame_gui/elements/ui_text_box.py
@@ -1,12 +1,19 @@
 import warnings
 import math
 import html
+import re
 
 from typing import Union, Tuple, Dict, Optional, Any
 
 import pygame
 
+from pygame import MOUSEBUTTONDOWN, MOUSEBUTTONUP, BUTTON_LEFT, KEYDOWN
+from pygame import KMOD_SHIFT, KMOD_META, KMOD_CTRL, KMOD_ALT, K_a, K_c
+from pygame import K_LEFT, K_RIGHT, K_UP, K_DOWN, K_HOME, K_END
+from pygame.event import Event
+
 from pygame_gui.core import ObjectID
+from pygame_gui.core.utility import clipboard_copy
 from pygame_gui._constants import UI_TEXT_BOX_LINK_CLICKED, OldType, UITextEffectType
 from pygame_gui._constants import TEXT_EFFECT_TYPING_APPEAR, TEXT_EFFECT_TILT
 from pygame_gui._constants import TEXT_EFFECT_FADE_IN, TEXT_EFFECT_FADE_OUT, TEXT_EFFECT_BOUNCE
@@ -111,6 +118,8 @@ def __init__(self,
                          object_id=object_id,
                          element_id=['text_box'])
         self.should_html_unescape_input_text = should_html_unescape_input_text
+        self.html_text = ""
+        self._plain_text = ""
         if self.should_html_unescape_input_text:
             self.html_text = html.unescape(html_text)
         else:
@@ -139,6 +148,8 @@ def __init__(self,
         self.border_colour = None
         self.line_spacing = 1.25
         self.text_cursor_colour = pygame.Color("white")
+        self.selected_bg_colour = pygame.Color(128, 128, 200, 255)
+        self.selected_text_colour = pygame.Color(255, 255, 255, 255)
 
         self.link_normal_colour = None
         self.link_hover_colour = None
@@ -166,8 +177,38 @@ def __init__(self,
 
         self.parser = None  # type: Optional[HTMLParser]
 
+        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_has_moved_recently = False
+
+        self.vertical_cursor_movement = False
+        self.last_horiz_cursor_index = 0
+
         self.rebuild_from_changed_theme_data()
 
+    @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 kill(self):
         """
         Overrides the standard sprite kill method to also kill any scroll bars belonging to this
@@ -390,6 +431,47 @@ def update(self, time_delta: float):
         super().update(time_delta)
         if not self.alive():
             return
+
+        if self.double_click_timer < self.ui_manager.get_double_click_time():
+            self.double_click_timer += time_delta
+
+        if self.selection_in_progress:
+            scaled_mouse_pos = self.ui_manager.get_mouse_position()
+            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.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._handle_cursor_visibility()
+            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.scroll_bar is not None and self.scroll_bar.check_has_moved_recently():
             height_adjustment = int(self.scroll_bar.start_percentage *
                                     self.text_box_layout.layout_rect.height)
@@ -426,6 +508,7 @@ def update(self, time_delta: float):
                        drawable_area)
             self._set_image(new_image)
 
+        any_hyper_link_hovered = False
         if len(self.link_hover_chunks) > 0:
             mouse_x, mouse_y = self.ui_manager.get_mouse_position()
             should_redraw_from_layout = False
@@ -448,6 +531,7 @@ def update(self, time_delta: float):
                 if hover_rect.collidepoint(mouse_x, mouse_y) and self.rect.collidepoint(mouse_x,
                                                                                         mouse_y):
                     hovered_currently = True
+                    any_hyper_link_hovered = True
                 if chunk.is_hovered and not hovered_currently:
                     chunk.on_unhovered()
                     should_redraw_from_layout = True
@@ -458,6 +542,15 @@ def update(self, time_delta: float):
             if should_redraw_from_layout:
                 self.redraw_from_text_block()
 
+        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]))):
+
+                if not (any_hyper_link_hovered and not self.selection_in_progress):
+                    self.ui_manager.set_text_hovered(True)
+
         self.update_text_effect(time_delta)
 
         if self.should_trigger_full_rebuild and self.full_rebuild_countdown <= 0.0:
@@ -466,6 +559,10 @@ def update(self, time_delta: float):
         if self.full_rebuild_countdown > 0.0:
             self.full_rebuild_countdown -= time_delta
 
+    def _handle_cursor_visibility(self):
+        # do nothing in text box - this is for the text entry box
+        pass
+
     def on_fresh_drawable_shape_ready(self):
         """
         Called by an element's drawable shape when it has a new image surface ready for use,
@@ -597,6 +694,8 @@ def parse_html_into_style_data(self):
                                              text_direction=self.parser.default_style['direction'],
                                              editable=True)
         self.text_box_layout.set_cursor_colour(self.text_cursor_colour)
+        self.text_box_layout.selection_colour = self.selected_bg_colour
+        self.text_box_layout.selection_text_colour = self.selected_text_colour
         self.parser.empty_layout_queue()
         if self.dynamic_height:
             self.text_box_layout.view_rect.height = self.text_box_layout.layout_rect.height
@@ -684,87 +783,246 @@ def full_redraw(self):
         self.link_hover_chunks = []
         self.text_box_layout.add_chunks_to_hover_group(self.link_hover_chunks)
 
-    def process_event(self, event: pygame.event.Event) -> bool:
+    def _process_mouse_button_event(self, event: Event) -> bool:
         """
-        Deals with input events. In this case we just handle clicks on any links in the text.
+        Process a mouse button event.
 
-        :param event: A pygame event to check for a reaction to.
+        :param event: Event to process.
 
-        :return: Returns True if we consumed this event.
+        :return: True if we consumed the mouse event.
 
         """
         consumed_event = False
         should_redraw_from_layout = False
-        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
+        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]):
-                consumed_event = True
-
                 if self.is_enabled:
-                    if self.scroll_bar is not None:
-                        text_block_full_height = self.text_box_layout.layout_rect.height
-                        height_adjustment = (self.scroll_bar.start_percentage *
-                                             text_block_full_height)
-                    else:
-                        height_adjustment = 0
-                    base_x = int(self.rect[0] + self.padding[0] + self.border_width +
-                                 self.shadow_width + self.rounded_corner_width_offsets[0])
-                    base_y = int(self.rect[1] + self.padding[1] + self.border_width +
-                                 self.shadow_width + self.rounded_corner_height_offsets[0] - height_adjustment)
-                    for chunk in self.link_hover_chunks:
-
-                        hover_rect = pygame.Rect((base_x + chunk.x,
-                                                  base_y + chunk.y),
-                                                 chunk.size)
-                        if hover_rect.collidepoint(scaled_mouse_pos[0], scaled_mouse_pos[1]):
-                            consumed_event = True
-                            if not chunk.is_active:
-                                chunk.set_active()
-                                should_redraw_from_layout = True
-
-        if self.is_enabled and event.type == pygame.MOUSEBUTTONUP and event.button == 1:
-            if self.scroll_bar is not None:
-                height_adjustment = (self.scroll_bar.start_percentage *
-                                     self.text_box_layout.layout_rect.height)
-            else:
-                height_adjustment = 0
-            base_x = int(self.rect[0] + self.padding[0] + self.border_width +
-                         self.shadow_width + self.rounded_corner_width_offsets[0])
-            base_y = int(self.rect[1] + self.padding[1] + self.border_width +
-                         self.shadow_width + self.rounded_corner_height_offsets[0] - height_adjustment)
+                    consumed_event = True
+                    should_redraw_from_layout = self._handle_hyper_link_mouse_button_down(scaled_mouse_pos)
+                    if not should_redraw_from_layout:
+                        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
+                                self.cursor_has_moved_recently = 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
+
+        if (event.type == MOUSEBUTTONUP and
+                event.button == BUTTON_LEFT):
             scaled_mouse_pos = self.ui_manager.calculate_scaled_mouse_position(event.pos)
-            for chunk in self.link_hover_chunks:
 
-                hover_rect = pygame.Rect((base_x + chunk.x,
-                                          base_y + chunk.y),
-                                         chunk.size)
-                if (hover_rect.collidepoint(scaled_mouse_pos[0], scaled_mouse_pos[1]) and
-                        self.rect.collidepoint(scaled_mouse_pos[0], scaled_mouse_pos[1])):
+            if self.hover_point(scaled_mouse_pos[0], scaled_mouse_pos[1]):
+                if self.is_enabled:
                     consumed_event = True
-                    if chunk.is_active:
+                    should_redraw_from_layout = self._handle_hyper_link_mouse_button_up(scaled_mouse_pos)
+                    if not should_redraw_from_layout and self.selection_in_progress:
+                        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
 
-                        # old event - to be removed in 0.8.0
-                        event_data = {'user_type': OldType(UI_TEXT_BOX_LINK_CLICKED),
-                                      'link_target': chunk.href,
-                                      'ui_element': self,
-                                      'ui_object_id': self.most_specific_combined_id}
-                        pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data))
+        if should_redraw_from_layout:
+            self.redraw_from_text_block()
 
-                        # new event
-                        event_data = {'link_target': chunk.href,
-                                      'ui_element': self,
-                                      'ui_object_id': self.most_specific_combined_id}
-                        pygame.event.post(pygame.event.Event(UI_TEXT_BOX_LINK_CLICKED, event_data))
+        return consumed_event
 
-                if chunk.is_active:
-                    chunk.set_inactive()
-                    should_redraw_from_layout = True
+    def process_event(self, event: Event) -> bool:
+        consumed_event = False
+        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_edit_pos_move_key(event):
+                consumed_event = True
+        return consumed_event
 
-        if should_redraw_from_layout:
-            self.redraw_from_text_block()
+    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 self._process_select_all_event(event):
+            consumed_event = True
+        elif self._process_copy_event(event):
+            consumed_event = True
+        return consumed_event
+
+    def _process_select_all_event(self, event: Event) -> bool:
+        """
+        Process a select all shortcut event. (CTRL+ A)
+
+        :param event: The event to process.
+
+        :return: True if the event is consumed.
+
+        """
+        consumed_event = False
+        if (event.key == K_a and
+                (event.mod & KMOD_CTRL or event.mod & KMOD_META) and
+                not (event.mod & KMOD_ALT)):  # hopefully enable diacritic letters
+            self._do_select_all()
+            consumed_event = True
+        return consumed_event
+
+    def _do_select_all(self):
+        if self.text_box_layout is not None:
+            self.select_range = [0, len(self.text_box_layout.plain_text)]
+            self.edit_position = len(self.text_box_layout.plain_text)
+            self.cursor_has_moved_recently = True
+
+    def _process_copy_event(self, event: Event) -> bool:
+        """
+        Process a copy shortcut event. (CTRL+ C)
+
+        :param event: The event to process.
 
+        :return: True if the event is consumed.
+
+        """
+        consumed_event = False
+        if (event.key == K_c and
+                (event.mod & KMOD_CTRL or event.mod & KMOD_META) and
+                not (event.mod & KMOD_ALT)):
+            self._do_copy()
+            consumed_event = True
         return consumed_event
 
+    def _do_copy(self):
+        if self.text_box_layout is not None and 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.text_box_layout.plain_text[low_end:high_end])
+
+    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] or self.text_box_layout is None:
+            return False
+        index = min(self.edit_position, len(self.text_box_layout.plain_text) - 1)
+        if index >= 0:
+            char = self.text_box_layout.plain_text[index]
+            # Check we clicked in the same place on our second click.
+            pattern = re.compile(r"[\w']+")
+
+            while index >= 0 and not pattern.match(char):
+                index -= 1
+                if index >= 0:
+                    char = self.text_box_layout.plain_text[index]
+                else:
+                    break
+            while index >= 0 and pattern.match(char):
+                index -= 1
+                if index >= 0:
+                    char = self.text_box_layout.plain_text[index]
+                else:
+                    break
+            start_select_index = index + 1
+            index += 1
+            if index < len(self.text_box_layout.plain_text):
+                char = self.text_box_layout.plain_text[index]
+                while index < len(self.text_box_layout.plain_text) and pattern.match(char):
+                    index += 1
+                    if index < len(self.text_box_layout.plain_text):
+                        char = self.text_box_layout.plain_text[index]
+            end_select_index = index
+            self.select_range = [start_select_index, end_select_index]
+            self.edit_position = end_select_index
+            self.selection_in_progress = False
+            return True
+        else:
+            return False
+
+    def _calculate_text_space_pos(self, scaled_mouse_pos):
+        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_top_left = self.get_text_layout_top_left()
+        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 _calculate_hyperlinks_offsets(self):
+        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_top_left = self.get_text_layout_top_left()
+        base_x = int(text_layout_top_left[0])
+        base_y = int(text_layout_top_left[1] - height_adjustment)
+
+        return base_x, base_y
+
+    def _handle_hyper_link_mouse_button_down(self, scaled_mouse_pos: Tuple[int, int]) -> bool:
+        should_redraw_from_layout = False
+        base_x, base_y = self._calculate_hyperlinks_offsets()
+        for chunk in self.link_hover_chunks:
+
+            hover_rect = pygame.Rect((base_x + chunk.x,
+                                      base_y + chunk.y),
+                                     chunk.size)
+            if hover_rect.collidepoint(scaled_mouse_pos[0], scaled_mouse_pos[1]):
+                if not chunk.is_active:
+                    chunk.set_active()
+                    should_redraw_from_layout = True
+        return should_redraw_from_layout
+
+    def _handle_hyper_link_mouse_button_up(self, scaled_mouse_pos: Tuple[int, int]) -> bool:
+        should_redraw_from_layout = False
+        base_x, base_y = self._calculate_hyperlinks_offsets()
+
+        for chunk in self.link_hover_chunks:
+
+            hover_rect = pygame.Rect((base_x + chunk.x,
+                                      base_y + chunk.y),
+                                     chunk.size)
+            if (hover_rect.collidepoint(scaled_mouse_pos[0], scaled_mouse_pos[1]) and
+                    self.rect.collidepoint(scaled_mouse_pos[0], scaled_mouse_pos[1])):
+
+                if chunk.is_active:
+                    # old event - to be removed in 0.8.0
+                    event_data = {'user_type': OldType(UI_TEXT_BOX_LINK_CLICKED),
+                                  'link_target': chunk.href,
+                                  'ui_element': self,
+                                  'ui_object_id': self.most_specific_combined_id}
+                    pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data))
+
+                    # new event
+                    event_data = {'link_target': chunk.href,
+                                  'ui_element': self,
+                                  'ui_object_id': self.most_specific_combined_id}
+                    pygame.event.post(pygame.event.Event(UI_TEXT_BOX_LINK_CLICKED, event_data))
+
+            if chunk.is_active:
+                chunk.set_inactive()
+                should_redraw_from_layout = True
+        return should_redraw_from_layout
+
     def rebuild_from_changed_theme_data(self):
         """
         Called by the UIManager to check the theming data and rebuild whatever needs rebuilding
@@ -812,6 +1070,24 @@ def rebuild_from_changed_theme_data(self):
             self.border_colour = border_colour
             has_any_changed = True
 
+        text_cursor_colour = self.ui_theme.get_colour_or_gradient('text_cursor',
+                                                                  self.combined_element_ids)
+        if text_cursor_colour != self.text_cursor_colour:
+            self.text_cursor_colour = text_cursor_colour
+            has_any_changed = True
+
+        selected_bg_colour = self.ui_theme.get_colour_or_gradient('selected_bg',
+                                                                  self.combined_element_ids)
+        if selected_bg_colour != self.selected_bg_colour:
+            self.selected_bg_colour = selected_bg_colour
+            has_any_changed = True
+
+        selected_text_colour = self.ui_theme.get_colour_or_gradient('selected_text',
+                                                                    self.combined_element_ids)
+        if selected_text_colour != self.selected_text_colour:
+            self.selected_text_colour = selected_text_colour
+            has_any_changed = True
+
         if self._check_text_alignment_theming():
             has_any_changed = True
 
@@ -1186,3 +1462,326 @@ def _pre_parse_text(self, input_text: str):
             pre_parsed_text = pre_parsed_text.replace('\n', '<br>')
 
         return pre_parsed_text
+
+    def unfocus(self):
+        """
+        Called when this element is no longer the current focus.
+        """
+        super().unfocus()
+
+    def focus(self):
+        """
+        Called when we 'select focus' on this element.
+        """
+        super().focus()
+        self.cursor_has_moved_recently = True
+
+    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
+        # 4 left/right cases:
+        # 1. jump left or right one character (LEFT or RIGHT on their own)
+        # 2. jump to end/start of word (CTRL + LEFT or RIGHT)
+        # 3. jump to end/start of line (HOME or END)
+        # 4. jump to end/start of document (CTRL + HOME or END)
+        # plus 1 up or down a line/text row case (UP & DOWN arrows, CTRL makes no difference here in notepad,
+        # but in IDE it scrolls if possible)
+        # All cases can have selection attached with shift held
+
+        if event.key == K_LEFT:
+            if event.mod & KMOD_CTRL or event.mod & KMOD_META:
+                self._jump_edit_pos_to_start_of_word(should_select=(event.mod & KMOD_SHIFT))
+            else:
+                self._jump_edit_pos_one_character_left(should_select=(event.mod & KMOD_SHIFT))
+            consumed_event = True
+        elif event.key == K_RIGHT:
+            if event.mod & KMOD_CTRL or event.mod & KMOD_META:
+                self._jump_edit_pos_to_end_of_word(should_select=(event.mod & KMOD_SHIFT))
+            else:
+                self._jump_edit_pos_one_character_right(should_select=(event.mod & KMOD_SHIFT))
+            consumed_event = True
+        elif event.key == K_UP:
+            self._jump_edit_pos_one_row_up(should_select=(event.mod & KMOD_SHIFT))
+            consumed_event = True
+        elif event.key == K_DOWN:
+            self._jump_edit_pos_one_row_down(should_select=(event.mod & KMOD_SHIFT))
+            consumed_event = True
+        elif event.key == K_HOME:
+            if event.mod & KMOD_CTRL or event.mod & KMOD_META:
+                self._jump_edit_pos_to_start_of_all_text(should_select=(event.mod & KMOD_SHIFT))
+            else:
+                self._jump_edit_pos_to_start_of_line(should_select=(event.mod & KMOD_SHIFT))
+            consumed_event = True
+        elif event.key == K_END:
+            if event.mod & KMOD_CTRL or event.mod & KMOD_META:
+                self._jump_edit_pos_to_end_of_all_text(should_select=(event.mod & KMOD_SHIFT))
+            else:
+                self._jump_edit_pos_to_end_of_line(should_select=(event.mod & KMOD_SHIFT))
+            consumed_event = True
+        return consumed_event
+
+    def _jump_edit_pos_one_character_right(self, should_select=False):
+        if self.text_box_layout is not None:
+            if should_select:
+                if abs(self.select_range[0] - self.select_range[1]) > 0:
+                    if self.edit_position == self.select_range[1]:
+                        # try to extend to the right
+                        self.select_range = [self.select_range[0],
+                                             min(len(self.text_box_layout.plain_text), self.edit_position + 1)]
+                    elif self.edit_position == self.select_range[0]:
+                        # reduce to the right
+                        self.select_range = [min(len(self.text_box_layout.plain_text), self.edit_position + 1),
+                                             self.select_range[1]]
+                else:
+                    self.select_range = [self.edit_position, min(len(self.text_box_layout.plain_text),
+                                                                 self.edit_position + 1)]
+                if self.edit_position < len(self.text_box_layout.plain_text):
+                    self.edit_position += 1
+                    self.cursor_has_moved_recently = True
+            else:
+                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.text_box_layout.plain_text):
+                    self.edit_position += 1
+                    self.cursor_has_moved_recently = True
+
+    def _jump_edit_pos_one_character_left(self, should_select=False):
+        if should_select:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                # existing selection, so edit_position should correspond to one
+                # end of it
+                if self.edit_position == self.select_range[0]:
+                    # try to extend to the left
+                    self.select_range = [max(0, self.edit_position - 1),
+                                         self.select_range[1]]
+                elif self.edit_position == self.select_range[1]:
+                    # reduce to the left
+                    self.select_range = [self.select_range[0],
+                                         max(0, self.edit_position - 1)]
+            else:
+                self.select_range = [max(0, self.edit_position - 1),
+                                     self.edit_position]
+            if self.edit_position > 0:
+                self.edit_position -= 1
+                self.cursor_has_moved_recently = True
+        else:
+            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
+
+    def _jump_edit_pos_one_row_down(self, should_select=False):
+        if self.text_box_layout is not None:
+            if should_select:
+                new_pos = self.text_box_layout.get_cursor_pos_move_down_one_row(self.last_horiz_cursor_index)
+                if abs(self.select_range[0] - self.select_range[1]) > 0:
+                    if self.edit_position == self.select_range[1]:
+                        # try to extend to the right
+                        self.select_range = [self.select_range[0],
+                                             min(len(self.text_box_layout.plain_text), new_pos)]
+                    elif self.edit_position == self.select_range[0]:
+                        # reduce to the right
+                        self.select_range = [min(len(self.text_box_layout.plain_text), new_pos),
+                                             self.select_range[1]]
+                else:
+                    self.select_range = [self.edit_position, min(len(self.text_box_layout.plain_text),
+                                                                 new_pos)]
+                if self.edit_position < len(self.text_box_layout.plain_text):
+                    self.edit_position = new_pos
+                    self.cursor_has_moved_recently = True
+            else:
+                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
+
+    def _jump_edit_pos_one_row_up(self, should_select=False):
+        if should_select:
+            new_pos = self.text_box_layout.get_cursor_pos_move_up_one_row(self.last_horiz_cursor_index)
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                # existing selection, so edit_position should correspond to one
+                # end of it
+                if self.edit_position == self.select_range[0]:
+                    # try to extend to the left
+                    self.select_range = [max(0, new_pos),
+                                         self.select_range[1]]
+                elif self.edit_position == self.select_range[1]:
+                    # reduce to the left
+                    self.select_range = [self.select_range[0],
+                                         max(0, new_pos)]
+            else:
+                self.select_range = [max(0, new_pos),
+                                     self.edit_position]
+            if self.edit_position > 0:
+                self.edit_position = new_pos
+                self.cursor_has_moved_recently = True
+        else:
+            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
+
+    def _jump_edit_pos_to_start_of_word(self, should_select=False):
+        if self.text_box_layout is not None:
+            try:
+                text_to_search = self.text_box_layout.plain_text[max(0, self.edit_position - 1)::-1]
+                match_result = re.search(r'\b\w+\b', text_to_search)
+                if match_result is not None:
+                    next_pos = max(self.edit_position - match_result.regs[-1][1], 0)
+                else:
+                    next_pos = self.edit_position
+            except ValueError:
+                next_pos = 0
+            # include case when shift held down to select everything to start
+            # of current line.
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                if should_select:
+                    if self.edit_position == self.select_range[1]:
+                        # undo selection to right, create to left
+                        self.select_range = [next_pos, self.select_range[0]]
+                    else:
+                        # extend left
+                        self.select_range = [next_pos, self.select_range[1]]
+                else:
+                    next_pos = self.select_range[0]
+                    self.select_range = [0, 0]
+            else:
+                if should_select:
+                    self.select_range = [next_pos, self.edit_position]
+                else:
+                    self.select_range = [0, 0]
+            self.edit_position = next_pos
+            self.cursor_has_moved_recently = True
+
+    def _jump_edit_pos_to_end_of_word(self, should_select=False):
+        if self.text_box_layout is not None:
+            try:
+                match_result = re.search(r'\b\w+\b', self.text_box_layout.plain_text[self.edit_position:])
+                if match_result is not None:
+                    next_pos = min(self.edit_position + match_result.regs[0][1], len(self.text_box_layout.plain_text))
+                else:
+                    next_pos = self.edit_position
+            except ValueError:
+                next_pos = len(self.text_box_layout.plain_text)
+            # include case when shift held down to select everything to end
+            # of current line.
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                if should_select:
+                    if self.edit_position == self.select_range[0]:
+                        # undo selection to left, create to right
+                        self.select_range = [self.select_range[1], next_pos]
+                    else:
+                        # extend right
+                        self.select_range = [self.select_range[0], next_pos]
+                else:
+                    self.select_range = [0, 0]
+            else:
+                if should_select:
+                    self.select_range = [self.edit_position, next_pos]
+                else:
+                    self.select_range = [0, 0]
+            self.edit_position = next_pos
+            self.cursor_has_moved_recently = True
+
+    def _jump_edit_pos_to_start_of_line(self, should_select=False):
+        try:
+            next_pos = self.text_box_layout.set_cursor_to_start_of_current_row()
+        except ValueError:
+            next_pos = 0
+        # include case when shift held down to select everything to start
+        # of current line.
+        if abs(self.select_range[0] - self.select_range[1]) > 0:
+            if should_select:
+                if self.edit_position == self.select_range[1]:
+                    # undo selection to right, create to left
+                    self.select_range = [next_pos, self.select_range[0]]
+                else:
+                    # extend left
+                    self.select_range = [next_pos, self.select_range[1]]
+            else:
+                self.select_range = [0, 0]
+        else:
+            if should_select:
+                self.select_range = [next_pos, self.edit_position]
+            else:
+                self.select_range = [0, 0]
+        self.edit_position = next_pos
+        self.cursor_has_moved_recently = True
+
+    def _jump_edit_pos_to_end_of_line(self, should_select=False):
+        if self.text_box_layout is not None:
+            try:
+                next_pos = self.text_box_layout.set_cursor_to_end_of_current_row()
+            except ValueError:
+                next_pos = len(self.text_box_layout.plain_text)
+            # include case when shift held down to select everything to end
+            # of current line.
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                if should_select:
+                    if self.edit_position == self.select_range[0]:
+                        # undo selection to left, create to right
+                        self.select_range = [self.select_range[1], next_pos]
+                    else:
+                        # extend right
+                        self.select_range = [self.select_range[0], next_pos]
+                else:
+                    self.select_range = [0, 0]
+            else:
+                if should_select:
+                    self.select_range = [self.edit_position, next_pos]
+                else:
+                    self.select_range = [0, 0]
+            self.edit_position = next_pos
+            self.cursor_has_moved_recently = True
+
+    def _jump_edit_pos_to_start_of_all_text(self, should_select=False):
+        if should_select:
+            if abs(self.select_range[0] - self.select_range[1]) > 0:
+                if self.edit_position == self.select_range[0]:
+                    self.select_range = [0, self.select_range[1]]
+                else:
+                    self.select_range = [0, self.select_range[0]]
+            else:
+                self.select_range = [0, self.edit_position]
+        else:
+            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
+
+    def _jump_edit_pos_to_end_of_all_text(self, should_select=False):
+        if self.text_box_layout is not None:
+            end_of_all_pos = len(self.text_box_layout.plain_text)
+            if should_select:
+                if abs(self.select_range[0] - self.select_range[1]) > 0:
+                    if self.edit_position == self.select_range[0]:
+                        self.select_range = [self.select_range[1], end_of_all_pos]
+                    else:
+                        self.select_range = [self.select_range[0], end_of_all_pos]
+                else:
+                    self.select_range = [self.edit_position, end_of_all_pos]
+            else:
+                if abs(self.select_range[0] - self.select_range[1]) > 0:
+                    self.select_range = [0, 0]
+            self.edit_position = end_of_all_pos
+            self.cursor_has_moved_recently = True
diff --git a/pygame_gui/elements/ui_text_entry_box.py b/pygame_gui/elements/ui_text_entry_box.py
index 67c4d9f3..70829a5a 100644
--- a/pygame_gui/elements/ui_text_entry_box.py
+++ b/pygame_gui/elements/ui_text_entry_box.py
@@ -1,11 +1,10 @@
-import re
 from typing import Union, Tuple, Dict, Optional
 
 from pygame_gui.core.utility import clipboard_paste, clipboard_copy
 
-from pygame import Rect, MOUSEBUTTONDOWN, MOUSEBUTTONUP, BUTTON_LEFT, KEYDOWN, TEXTINPUT
-from pygame import KMOD_SHIFT, KMOD_META, KMOD_CTRL, KMOD_ALT, 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 Rect, KEYDOWN, TEXTINPUT
+from pygame import KMOD_META, KMOD_CTRL, KMOD_ALT, K_x, K_v
+from pygame import 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
@@ -67,16 +66,7 @@ def __init__(self,
         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:
         """
@@ -92,36 +82,15 @@ def set_text(self, html_text: str, *, text_kwargs: Optional[Dict[str, str]] = No
         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.cursor_has_moved_recently = False
         self.redraw_from_text_block()
 
     def focus(self):
@@ -147,52 +116,6 @@ def update(self, time_delta: float):
 
         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:
@@ -210,18 +133,14 @@ def update(self, time_delta: float):
         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 _handle_cursor_visibility(self):
+        self.cursor_blink_delay_after_moving_acc = 0.0
+        self.cursor_on = True
+        self.text_box_layout.turn_on_cursor()
 
     def process_event(self, event: Event) -> bool:
         """
-        Allows the text entry box to react to input events, which is it's primary function.
+        Allows the text entry box to react to input events, which is its 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
@@ -331,361 +250,6 @@ def _process_action_key_event(self, event: Event) -> bool:
 
         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
-        # 4 left/right cases:
-        # 1. jump left or right one character (LEFT or RIGHT on their own)
-        # 2. jump to end/start of word (CTRL + LEFT or RIGHT)
-        # 3. jump to end/start of line (HOME or END)
-        # 4. jump to end/start of document (CTRL + HOME or END)
-        # plus 1 up or down a line/text row case (UP & DOWN arrows, CTRL makes no difference here in notepad,
-        # but in IDE it scrolls if possible)
-        # All cases can have selection attached with shift held
-
-        if event.key == K_LEFT:
-            if event.mod & KMOD_CTRL or event.mod & KMOD_META:
-                self._jump_edit_pos_to_start_of_word(should_select=(event.mod & KMOD_SHIFT))
-            else:
-                self._jump_edit_pos_one_character_left(should_select=(event.mod & KMOD_SHIFT))
-            consumed_event = True
-        elif event.key == K_RIGHT:
-            if event.mod & KMOD_CTRL or event.mod & KMOD_META:
-                self._jump_edit_pos_to_end_of_word(should_select=(event.mod & KMOD_SHIFT))
-            else:
-                self._jump_edit_pos_one_character_right(should_select=(event.mod & KMOD_SHIFT))
-            consumed_event = True
-        elif event.key == K_UP:
-            self._jump_edit_pos_one_row_up(should_select=(event.mod & KMOD_SHIFT))
-            consumed_event = True
-        elif event.key == K_DOWN:
-            self._jump_edit_pos_one_row_down(should_select=(event.mod & KMOD_SHIFT))
-            consumed_event = True
-        elif event.key == K_HOME:
-            if event.mod & KMOD_CTRL or event.mod & KMOD_META:
-                self._jump_edit_pos_to_start_of_all_text(should_select=(event.mod & KMOD_SHIFT))
-            else:
-                self._jump_edit_pos_to_start_of_line(should_select=(event.mod & KMOD_SHIFT))
-            consumed_event = True
-        elif event.key == K_END:
-            if event.mod & KMOD_CTRL or event.mod & KMOD_META:
-                self._jump_edit_pos_to_end_of_all_text(should_select=(event.mod & KMOD_SHIFT))
-            else:
-                self._jump_edit_pos_to_end_of_line(should_select=(event.mod & KMOD_SHIFT))
-            consumed_event = True
-        return consumed_event
-
-    def _jump_edit_pos_one_character_right(self, should_select=False):
-        if should_select:
-            if abs(self.select_range[0] - self.select_range[1]) > 0:
-                if self.edit_position == self.select_range[1]:
-                    # try to extend to the right
-                    self.select_range = [self.select_range[0],
-                                         min(len(self.html_text), self.edit_position + 1)]
-                elif self.edit_position == self.select_range[0]:
-                    # reduce to the right
-                    self.select_range = [min(len(self.html_text), self.edit_position + 1),
-                                         self.select_range[1]]
-            else:
-                self.select_range = [self.edit_position, min(len(self.html_text),
-                                                             self.edit_position + 1)]
-            if self.edit_position < len(self.html_text):
-                self.edit_position += 1
-                self.cursor_has_moved_recently = True
-        else:
-            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
-
-    def _jump_edit_pos_one_character_left(self, should_select=False):
-        if should_select:
-            if abs(self.select_range[0] - self.select_range[1]) > 0:
-                # existing selection, so edit_position should correspond to one
-                # end of it
-                if self.edit_position == self.select_range[0]:
-                    # try to extend to the left
-                    self.select_range = [max(0, self.edit_position - 1),
-                                         self.select_range[1]]
-                elif self.edit_position == self.select_range[1]:
-                    # reduce to the left
-                    self.select_range = [self.select_range[0],
-                                         max(0, self.edit_position - 1)]
-            else:
-                self.select_range = [max(0, self.edit_position - 1),
-                                     self.edit_position]
-            if self.edit_position > 0:
-                self.edit_position -= 1
-                self.cursor_has_moved_recently = True
-        else:
-            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
-
-    def _jump_edit_pos_one_row_down(self, should_select=False):
-        if should_select:
-            new_pos = self.text_box_layout.get_cursor_pos_move_down_one_row(self.last_horiz_cursor_index)
-            if abs(self.select_range[0] - self.select_range[1]) > 0:
-                if self.edit_position == self.select_range[1]:
-                    # try to extend to the right
-                    self.select_range = [self.select_range[0],
-                                         min(len(self.html_text), new_pos)]
-                elif self.edit_position == self.select_range[0]:
-                    # reduce to the right
-                    self.select_range = [min(len(self.html_text), new_pos),
-                                         self.select_range[1]]
-            else:
-                self.select_range = [self.edit_position, min(len(self.html_text),
-                                                             new_pos)]
-            if self.edit_position < len(self.html_text):
-                self.edit_position = new_pos
-                self.cursor_has_moved_recently = True
-        else:
-            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
-
-    def _jump_edit_pos_one_row_up(self, should_select=False):
-        if should_select:
-            new_pos = self.text_box_layout.get_cursor_pos_move_up_one_row(self.last_horiz_cursor_index)
-            if abs(self.select_range[0] - self.select_range[1]) > 0:
-                # existing selection, so edit_position should correspond to one
-                # end of it
-                if self.edit_position == self.select_range[0]:
-                    # try to extend to the left
-                    self.select_range = [max(0, new_pos),
-                                         self.select_range[1]]
-                elif self.edit_position == self.select_range[1]:
-                    # reduce to the left
-                    self.select_range = [self.select_range[0],
-                                         max(0, new_pos)]
-            else:
-                self.select_range = [max(0, new_pos),
-                                     self.edit_position]
-            if self.edit_position > 0:
-                self.edit_position = new_pos
-                self.cursor_has_moved_recently = True
-        else:
-            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
-
-    def _jump_edit_pos_to_start_of_word(self, should_select=False):
-        try:
-            text_to_search = self.html_text[max(0, self.edit_position-1)::-1]
-            match_result = re.search(r'\b\w+\b', text_to_search)
-            if match_result is not None:
-                next_pos = max(self.edit_position - match_result.regs[-1][1], 0)
-            else:
-                next_pos = self.edit_position
-        except ValueError:
-            next_pos = 0
-        # include case when shift held down to select everything to start
-        # of current line.
-        if abs(self.select_range[0] - self.select_range[1]) > 0:
-            if should_select:
-                if self.edit_position == self.select_range[1]:
-                    # undo selection to right, create to left
-                    self.select_range = [next_pos, self.select_range[0]]
-                else:
-                    # extend left
-                    self.select_range = [next_pos, self.select_range[1]]
-            else:
-                next_pos = self.select_range[0]
-                self.select_range = [0, 0]
-        else:
-            if should_select:
-                self.select_range = [next_pos, self.edit_position]
-            else:
-                self.select_range = [0, 0]
-        self.edit_position = next_pos
-        self.cursor_has_moved_recently = True
-
-    def _jump_edit_pos_to_end_of_word(self, should_select=False):
-        try:
-            match_result = re.search(r'\b\w+\b', self.html_text[self.edit_position:])
-            if match_result is not None:
-                next_pos = min(self.edit_position + match_result.regs[0][1], len(self.html_text))
-            else:
-                next_pos = self.edit_position
-        except ValueError:
-            next_pos = len(self.html_text)
-        # include case when shift held down to select everything to end
-        # of current line.
-        if abs(self.select_range[0] - self.select_range[1]) > 0:
-            if should_select:
-                if self.edit_position == self.select_range[0]:
-                    # undo selection to left, create to right
-                    self.select_range = [self.select_range[1], next_pos]
-                else:
-                    # extend right
-                    self.select_range = [self.select_range[0], next_pos]
-            else:
-                self.select_range = [0, 0]
-        else:
-            if should_select:
-                self.select_range = [self.edit_position, next_pos]
-            else:
-                self.select_range = [0, 0]
-        self.edit_position = next_pos
-        self.cursor_has_moved_recently = True
-
-    def _jump_edit_pos_to_start_of_line(self, should_select=False):
-        try:
-            next_pos = self.text_box_layout.set_cursor_to_start_of_current_row()
-        except ValueError:
-            next_pos = 0
-        # include case when shift held down to select everything to start
-        # of current line.
-        if abs(self.select_range[0] - self.select_range[1]) > 0:
-            if should_select:
-                if self.edit_position == self.select_range[1]:
-                    # undo selection to right, create to left
-                    self.select_range = [next_pos, self.select_range[0]]
-                else:
-                    # extend left
-                    self.select_range = [next_pos, self.select_range[1]]
-            else:
-                self.select_range = [0, 0]
-        else:
-            if should_select:
-                self.select_range = [next_pos, self.edit_position]
-            else:
-                self.select_range = [0, 0]
-        self.edit_position = next_pos
-        self.cursor_has_moved_recently = True
-
-    def _jump_edit_pos_to_end_of_line(self, should_select=False):
-        try:
-            next_pos = self.text_box_layout.set_cursor_to_end_of_current_row()
-        except ValueError:
-            next_pos = len(self.html_text)
-        # include case when shift held down to select everything to end
-        # of current line.
-        if abs(self.select_range[0] - self.select_range[1]) > 0:
-            if should_select:
-                if self.edit_position == self.select_range[0]:
-                    # undo selection to left, create to right
-                    self.select_range = [self.select_range[1], next_pos]
-                else:
-                    # extend right
-                    self.select_range = [self.select_range[0], next_pos]
-            else:
-                self.select_range = [0, 0]
-        else:
-            if should_select:
-                self.select_range = [self.edit_position, next_pos]
-            else:
-                self.select_range = [0, 0]
-        self.edit_position = next_pos
-        self.cursor_has_moved_recently = True
-
-    def _jump_edit_pos_to_start_of_all_text(self, should_select=False):
-        if should_select:
-            if abs(self.select_range[0] - self.select_range[1]) > 0:
-                if self.edit_position == self.select_range[0]:
-                    self.select_range = [0, self.select_range[1]]
-                else:
-                    self.select_range = [0, self.select_range[0]]
-            else:
-                self.select_range = [0, self.edit_position]
-        else:
-            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
-
-    def _jump_edit_pos_to_end_of_all_text(self, should_select=False):
-        end_of_all_pos = len(self.html_text)
-        if should_select:
-            if abs(self.select_range[0] - self.select_range[1]) > 0:
-                if self.edit_position == self.select_range[0]:
-                    self.select_range = [self.select_range[1], end_of_all_pos]
-                else:
-                    self.select_range = [self.select_range[0], end_of_all_pos]
-            else:
-                self.select_range = [self.edit_position, end_of_all_pos]
-        else:
-            if abs(self.select_range[0] - self.select_range[1]) > 0:
-                self.select_range = [0, 0]
-        self.edit_position = end_of_all_pos
-        self.cursor_has_moved_recently = True
-
-    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.
@@ -738,39 +302,13 @@ def _process_keyboard_shortcut_event(self, event: Event) -> bool:
         :return: True if event consumed.
 
         """
-        consumed_event = False
-        if self._process_select_all_event(event):
-            consumed_event = True
-        elif self._process_cut_event(event):
-            consumed_event = True            
-        elif self._process_copy_event(event):
+        consumed_event = super()._process_keyboard_shortcut_event(event)
+        if self._process_cut_event(event):
             consumed_event = True
         elif self._process_paste_event(event):
             consumed_event = True
         return consumed_event
 
-    def _process_select_all_event(self, event: Event) -> bool:
-        """
-        Process a select all shortcut event. (CTRL+ A)
-
-        :param event: The event to process.
-
-        :return: True if the event is consumed.
-
-        """
-        consumed_event = False
-        if (event.key == K_a and
-                (event.mod & KMOD_CTRL or event.mod & KMOD_META) and
-                not (event.mod & KMOD_ALT)):  # hopefully enable diacritic letters)
-            self._do_select_all()
-            consumed_event = True
-        return consumed_event
-
-    def _do_select_all(self):
-        self.select_range = [0, len(self.html_text)]
-        self.edit_position = len(self.html_text)
-        self.cursor_has_moved_recently = True
-        
     def _process_cut_event(self, event: Event) -> bool:
         """
         Process a cut shortcut event. (CTRL+ X)
@@ -782,8 +320,8 @@ def _process_cut_event(self, event: Event) -> bool:
         """
         consumed_event = False
         if (event.key == K_x and
-              (event.mod & KMOD_CTRL or event.mod & KMOD_META) and
-              not (event.mod & KMOD_ALT)):
+                (event.mod & KMOD_CTRL or event.mod & KMOD_META) and
+                not (event.mod & KMOD_ALT)):
             self._do_cut()
             consumed_event = True
         return consumed_event
@@ -800,30 +338,7 @@ def _do_cut(self):
             self.select_range = [0, 0]
             self.redraw_from_text_block()
             self.cursor_has_moved_recently = True
-    
-    def _process_copy_event(self, event: Event) -> bool:
-        """
-        Process a copy shortcut event. (CTRL+ C)
-
-        :param event: The event to process.
-
-        :return: True if the event is consumed.
-
-        """
-        consumed_event = False
-        if (event.key == K_c and
-              (event.mod & KMOD_CTRL or event.mod & KMOD_META) and
-              not (event.mod & KMOD_ALT)):
-            self._do_copy()
-            consumed_event = True
-        return consumed_event
 
-    def _do_copy(self):
-        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])
-    
     def _process_paste_event(self, event: Event) -> bool:
         """
         Process a paste shortcut event. (CTRL+ V)
@@ -866,48 +381,6 @@ def _do_paste(self):
                 self.redraw_from_text_block()
                 self.cursor_has_moved_recently = True
 
-    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()
@@ -915,18 +388,3 @@ def redraw_from_text_block(self):
     @staticmethod
     def convert_all_line_endings_to_unix(input_str: str):
         return input_str.replace('\r\n', '\n').replace('\r', '\n')
-
-    def rebuild_from_changed_theme_data(self):
-        # TODO: refactor this a bit so we don't rebuild twice if base classes and this class has changed data.
-        super().rebuild_from_changed_theme_data()
-
-        has_any_changed = False
-
-        text_cursor_colour = self.ui_theme.get_colour_or_gradient('text_cursor',
-                                                                  self.combined_element_ids)
-        if text_cursor_colour != self.text_cursor_colour:
-            self.text_cursor_colour = text_cursor_colour
-            has_any_changed = True
-
-        if has_any_changed:
-            self._reparse_and_rebuild()
diff --git a/pygame_gui/elements/ui_text_entry_line.py b/pygame_gui/elements/ui_text_entry_line.py
index bf511aee..b4720278 100644
--- a/pygame_gui/elements/ui_text_entry_line.py
+++ b/pygame_gui/elements/ui_text_entry_line.py
@@ -324,7 +324,7 @@ def update(self, time_delta: float):
             return
         scaled_mouse_pos = self.ui_manager.get_mouse_position()
         if self.hovered:
-            self.ui_manager.set_text_input_hovered(True)
+            self.ui_manager.set_text_hovered(True)
 
         if self.double_click_timer < self.ui_manager.get_double_click_time():
             self.double_click_timer += time_delta
diff --git a/pygame_gui/ui_manager.py b/pygame_gui/ui_manager.py
index 63016737..e53a42fc 100644
--- a/pygame_gui/ui_manager.py
+++ b/pygame_gui/ui_manager.py
@@ -98,7 +98,7 @@ def __init__(self,
         self._load_default_cursors()
         self.active_user_cursor = pygame.cursors.Cursor(pygame.SYSTEM_CURSOR_ARROW)
         self._active_cursor = self.active_user_cursor
-        self.text_input_hovered = False
+        self.text_hovered = False
         self.hovering_any_ui_element = False
 
         if auto_load:
@@ -106,7 +106,7 @@ def __init__(self,
             # If we are using a blocking loader this will only return when loading is complete
             self.resource_loader.update()
         
-    def create_new_theme(self, theme_path: Union[str, os.PathLike, io.StringIO, PackageResource, dict]=None) -> UIAppearanceTheme:
+    def create_new_theme(self, theme_path: Union[str, os.PathLike, io.StringIO, PackageResource, dict] = None) -> UIAppearanceTheme:
         """
         Create a new theme using self information.
         :param theme_path: relative file path to theme or theme dictionary.
@@ -237,17 +237,19 @@ def process_events(self, event: pygame.event.Event):
                         consumed_event = ui_element.process_event(event)
                         if consumed_event:
                             # Generally clicks should only be handled by the top layer of whatever
-                            # GUI thing we are  clicking on. I am trusting UIElments to decide
+                            # GUI thing we are  clicking on. I am trusting UIElements to decide
                             # whether they need to consume the events they respond to. Hopefully
                             # this is not a mistake.
 
                             break
         return consumed_event
     
-    def set_ui_theme(self, theme:IUIAppearanceThemeInterface, update_all_sprites:bool=False):
+    def set_ui_theme(self, theme: IUIAppearanceThemeInterface, update_all_sprites: bool = False):
         """
         Set ui theme.
+
         :param theme: The theme to set.
+        :param update_all_sprites:
         """
         for sprite in self.ui_group.sprites():
             if not update_all_sprites and sprite.ui_theme is not self.ui_theme:
@@ -256,7 +258,7 @@ def set_ui_theme(self, theme:IUIAppearanceThemeInterface, update_all_sprites:boo
         self.ui_theme = theme
         self.rebuild_all_from_changed_theme_data(self.ui_theme)
     
-    def rebuild_all_from_changed_theme_data(self, theme:IUIAppearanceThemeInterface=None):
+    def rebuild_all_from_changed_theme_data(self, theme: IUIAppearanceThemeInterface = None):
         for sprite in self.ui_group.sprites():
             if theme is not None and sprite.ui_theme is not theme:
                 continue
@@ -265,7 +267,7 @@ def rebuild_all_from_changed_theme_data(self, theme:IUIAppearanceThemeInterface=
     def update(self, time_delta: float):
         """
         From here all our UI elements are updated and which element is currently 'hovered' is
-        checked; which means the mouse pointer is overlapping them. This is managed centrally so
+        checked; which means the mouse pointer is overlapping them. This is managed centrally, so
         we aren't ever overlapping two elements at once.
 
         It also updates the shape cache to continue storing already created elements shapes in the
@@ -292,12 +294,12 @@ 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.set_text_hovered(False)  # reset the text hovered status each loop
 
         self.ui_group.update(time_delta)
 
         # handle mouse cursors
-        if self.text_input_hovered:
+        if self.text_hovered:
             new_cursor = pygame.cursors.Cursor(pygame.SYSTEM_CURSOR_IBEAM)
             if new_cursor != self._active_cursor:
                 self._active_cursor = new_cursor
@@ -319,7 +321,7 @@ def update(self, time_delta: float):
                     except pygame.error:
                         pass
 
-        if (not any_window_edge_hovered and not self.text_input_hovered and
+        if (not any_window_edge_hovered and not self.text_hovered and
                 self._active_cursor != self.active_user_cursor):
             self._active_cursor = self.active_user_cursor
             try:
@@ -334,8 +336,8 @@ def _handle_hovering(self, time_delta):
             for ui_element in self.ui_group.get_sprites_from_layer(layer):
                 if ui_element.visible:
                     # Only check hover for visible elements - ignore hidden elements
-                    # we need to check hover even after already found what we are hovering
-                    # so we can unhover previously hovered stuff
+                    # we need to check hover even after already found what we are hovering,
+                    # so, we can unhover previously hovered stuff
                     element_is_hovered = ui_element.check_hover(time_delta, hover_handled)
                     if element_is_hovered and ui_element != self.root_container:
                         hover_handled = True
@@ -347,7 +349,7 @@ def _handle_hovering(self, time_delta):
 
     def get_mouse_position(self) -> Tuple[int, int]:
         """
-        Wrapping pygame mouse position so we can mess with it.
+        Wrapping pygame mouse position, so we can mess with it.
         """
         return self.mouse_position
 
@@ -377,7 +379,7 @@ def add_font_paths(self, font_name: str, regular_path: str, bold_path: str = Non
         The font name you specify here can be used to choose the font in the blocks of HTML-subset
         formatted text, available in some of the UI elements like the UITextBox.
 
-        It is recommended that you also pre-load any fonts you use at an appropriate moment in your
+        It is recommended that you also preload any fonts you use at an appropriate moment in your
         project rather than letting the library dynamically load them when they are required. That
         is because dynamic loading of large font files can cause UI elements with a lot of font
         usage to appear rather slowly as they are waiting for the fonts they need to load.
@@ -399,12 +401,12 @@ def add_font_paths(self, font_name: str, regular_path: str, bold_path: str = Non
 
     def preload_fonts(self, font_list: List[Dict[str, Union[str, int, float]]]):
         """
-        It's a good idea to pre-load the exact fonts your program uses during the loading phase of
-        your program. By default the pygame_gui library will still work, but will spit out reminder
+        It's a good idea to preload the exact fonts your program uses during the loading phase of
+        your program. By default, the pygame_gui library will still work, but will spit out reminder
         warnings when you haven't done this. Loading fonts on the fly will slow down the apparent
         responsiveness when creating UI elements that use a lot of different fonts.
 
-        To pre-load custom fonts, or to use custom fonts at all (i.e. ones that aren't the default
+        To preload custom fonts, or to use custom fonts at all (i.e. ones that aren't the default
         'noto_sans' font) you must first add the paths to the files for those fonts, then load the
         specific fonts with a list of font descriptions in a dictionary form like so:
 
@@ -458,12 +460,12 @@ def preload_fonts(self, font_list: List[Dict[str, Union[str, int, float]]]):
 
     def print_unused_fonts(self):
         """
-        Helps you identify which pre-loaded fonts you are actually still using in your project
+        Helps you identify which preloaded fonts you are actually still using in your project
         after you've fiddled around with the text a lot by printing out a list of fonts that have
         not been used yet at the time this function is called.
 
         Of course if you don't run the code path in which a particular font is used before calling
-        this function then it won't be of much use, so take it's results under advisement rather
+        this function then it won't be of much use, so take its results under advisement rather
         than as gospel.
 
         """
@@ -563,7 +565,7 @@ def get_universal_empty_surface(self) -> pygame.surface.Surface:
         Sometimes we want to hide sprites or just have sprites with no visual component, when we
         do we can just use this empty surface to save having lots of empty surfaces all over memory.
 
-        :return: An empty, and therefore invisible pygame.surface.Surface
+        :return: An empty and therefore invisible pygame.surface.Surface
         """
         return self.universal_empty_surface
 
@@ -577,7 +579,7 @@ def create_tool_tip(self,
                         wrap_width: Optional[int] = None,
                         text_kwargs: Optional[Dict[str, str]] = None) -> IUITooltipInterface:
         """
-        Creates a tool tip ands returns it. Have hidden this away in the manager so we can call it
+        Creates a tool tip ands returns it. Have hidden this away in the manager, so we can call it
         from other UI elements and create tool tips without creating cyclical import problems.
 
         :param text: The tool tips text, can utilise the HTML subset used in all UITextBoxes.
@@ -599,7 +601,7 @@ def create_tool_tip(self,
 
     def _update_mouse_position(self):
         """
-        Wrapping pygame mouse position so we can mess with it.
+        Wrapping pygame mouse position, so we can mess with it.
         """
         self.mouse_position = self.calculate_scaled_mouse_position(pygame.mouse.get_pos())
 
@@ -636,8 +638,8 @@ def set_locale(self, locale: str):
     def get_locale(self):
         return self._locale
 
-    def set_text_input_hovered(self, hovering_text_input: bool):
-        self.text_input_hovered = hovering_text_input
+    def set_text_hovered(self, hovering_text: bool):
+        self.text_hovered = hovering_text
 
     def get_hovering_any_element(self) -> bool:
         return self.hovering_any_ui_element


Test patch:
diff --git a/tests/data/themes/ui_text_box_non_default.json b/tests/data/themes/ui_text_box_non_default.json
index 095e41ed..8351bf4d 100644
--- a/tests/data/themes/ui_text_box_non_default.json
+++ b/tests/data/themes/ui_text_box_non_default.json
@@ -9,7 +9,8 @@
              "link_selected": "#FFFFFF",
              "normal_border": "#AA0AAA",
              "regular_text": "#D0D0D0,#FFFFFF,#D050FF,90",
-             "normal_text": "#80F080"
+             "normal_text": "#80F080",
+             "text_cursor": "#FF0000"
          },
          "font":
          {
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 ba2d6565..e9185a78 100644
--- a/tests/test_core/test_text/test_text_box_layout.py
+++ b/tests/test_core/test_text/test_text_box_layout.py
@@ -712,7 +712,7 @@ def test_set_cursor_position(self, _init_pygame, _display_surface_return_none, d
 
         assert layout.cursor_text_row is not None
         assert layout.cursor_text_row.cursor_index == 2
-        assert layout.cursor_text_row.cursor_draw_width == 17
+        assert layout.cursor_text_row.cursor_draw_width == 18
 
         layout.cursor_text_row.edit_cursor_active = True
         layout.set_cursor_position(10)
@@ -752,7 +752,7 @@ def test_set_cursor_from_click_pos(self, _init_pygame, _display_surface_return_n
         layout.set_cursor_from_click_pos((17, 24))
         assert layout.cursor_text_row is not None
         assert layout.cursor_text_row.cursor_index == 2
-        assert layout.cursor_text_row.cursor_draw_width == 17
+        assert layout.cursor_text_row.cursor_draw_width == 18
 
         layout.cursor_text_row.edit_cursor_active = True
         layout.set_cursor_from_click_pos((17, 24))
diff --git a/tests/test_elements/test_ui_text_box.py b/tests/test_elements/test_ui_text_box.py
index a51e8aba..a9458f6c 100644
--- a/tests/test_elements/test_ui_text_box.py
+++ b/tests/test_elements/test_ui_text_box.py
@@ -6,6 +6,7 @@
 import i18n
 
 import pygame_gui
+from pygame_gui.core.utility import clipboard_paste
 from pygame_gui.elements.ui_text_box import UITextBox
 from pygame_gui.ui_manager import UIManager
 from tests.shared_comparators import compare_surfaces
@@ -969,6 +970,26 @@ def test_pre_parsing(self, _init_pygame: None,
         assert text_box.image is not None
         assert len(text_box.text_box_layout.layout_rows) == 3
 
+    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 = UITextBox(html_text="dan",
+                               relative_rect=pygame.Rect(100, 100, 200, 30),
+                               manager=manager)
+
+        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'
+
 
 if __name__ == '__main__':
     pytest.console_main()
diff --git a/tests/test_elements/test_ui_text_entry_box.py b/tests/test_elements/test_ui_text_entry_box.py
index b639aeb8..20edf235 100644
--- a/tests/test_elements/test_ui_text_entry_box.py
+++ b/tests/test_elements/test_ui_text_entry_box.py
@@ -882,26 +882,6 @@ def test_process_event_text_entered_with_select_range(self, _init_pygame: None,
         assert (processed_text_input_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),
@@ -1539,6 +1519,62 @@ def test_process_event_text_left_select_range(self, _init_pygame: None,
 
         assert processed_key_event
 
+    def test_process_event_text_up_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\n'
+                            'dan')
+        text_entry.focus()
+        text_entry.select_range = [6, 7]
+        text_entry.edit_position = 6
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_UP,
+                                                                           'mod': 0}))
+
+        assert processed_key_event
+
+        text_entry.focus()
+        text_entry.select_range = [6, 7]
+        text_entry.edit_position = 6
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_UP,
+                                                                           'mod': pygame.KMOD_SHIFT}))
+
+        assert processed_key_event
+
+    def test_process_event_text_down_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\n'
+                            'dan')
+        text_entry.focus()
+        text_entry.select_range = [0, 2]
+        text_entry.edit_position = 2
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_DOWN,
+                                                                           'mod': 0}))
+
+        assert processed_key_event
+
+        text_entry.focus()
+        text_entry.select_range = [0, 2]
+        text_entry.edit_position = 2
+
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
+                                                                          {'key': pygame.K_DOWN,
+                                                                           'mod': pygame.KMOD_SHIFT}))
+
+        assert processed_key_event
+
     def test_process_event_delete_select_range(self, _init_pygame: None,
                                                default_ui_manager: UIManager,
                                                _display_surface_return_none: None):
@@ -1640,11 +1676,11 @@ def _update_mouse_position(self):
         text_entry.update(0.01)
 
         assert text_entry.alive()
-        assert not manager.text_input_hovered
+        assert not manager.text_hovered
 
         MouselessManager.fixed_mouse_pos = (200, 115)
         manager.update(0.01)
-        assert manager.text_input_hovered
+        assert manager.text_hovered
 
         text_entry.kill()
 
diff --git a/tests/test_elements/test_ui_text_entry_line.py b/tests/test_elements/test_ui_text_entry_line.py
index b4ce92f0..cdaae137 100644
--- a/tests/test_elements/test_ui_text_entry_line.py
+++ b/tests/test_elements/test_ui_text_entry_line.py
@@ -959,11 +959,11 @@ def _update_mouse_position(self):
         text_entry.update(0.01)
 
         assert text_entry.alive()
-        assert not manager.text_input_hovered
+        assert not manager.text_hovered
 
         MouselessManager.fixed_mouse_pos = (200, 115)
         manager.update(0.01)
-        assert manager.text_input_hovered
+        assert manager.text_hovered
 
         text_entry.kill()