Repository: MyreMylar/pygame_gui (language: python)

Issue:
Filled Bars e.g. Progress Bars
**Is your feature request related to a problem? Please describe.**
Personally, I think the current healthbars are too proscriptive as they rely on the dev working ina  specific way. The rest of the UI Elements serve broad purposes with options to customise and as such can be tailored to fit the need.

**Describe the solution you'd like**
Similar to UIScreenSpaceHealthBar, but abstracted another layer. 

Simply put, a UI Element that we can size (as normal) and that takes a "% filled" that determines how much the outer, border rectangle shape is filled with the inner colour (as set by the theme). The inner fill could also be an image that is stretched or tiled.




Discussion / hints:
This may have snuck in to 0.6.0 as a hidden feature. 

I'm not sure when it was added exactly or if it works... Looks to be called `UIProgressBar`

Fix patch:
diff --git a/docs/source/pygame_gui.elements.rst b/docs/source/pygame_gui.elements.rst
index 2e297cb8..35d337d0 100644
--- a/docs/source/pygame_gui.elements.rst
+++ b/docs/source/pygame_gui.elements.rst
@@ -52,6 +52,14 @@ pygame\_gui.elements.ui\_panel module
    :no-undoc-members:
    :show-inheritance:
 
+pygame\_gui.elements.ui\_progress\_bar module
+---------------------------------------------
+
+.. automodule:: pygame_gui.elements.ui_progress_bar
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
 pygame\_gui.elements.ui\_screen\_space\_health\_bar module
 ----------------------------------------------------------
 
@@ -68,6 +76,14 @@ pygame\_gui.elements.ui\_selection\_list module
    :no-undoc-members:
    :show-inheritance:
 
+pygame\_gui.elements.ui\_status\_bar module
+----------------------------------------------------------
+
+.. automodule:: pygame_gui.elements.ui_status_bar
+   :members:
+   :no-undoc-members:
+   :show-inheritance:
+
 pygame\_gui.elements.ui\_text\_box module
 -----------------------------------------
 
diff --git a/docs/source/theme_guide.rst b/docs/source/theme_guide.rst
index 95e0b6cc..2bdeffe6 100644
--- a/docs/source/theme_guide.rst
+++ b/docs/source/theme_guide.rst
@@ -312,9 +312,11 @@ Theme Options Per Element
     theme_reference/theme_image
     theme_reference/theme_label
     theme_reference/theme_panel
+    theme_reference/theme_progress_bar
     theme_reference/theme_screen_space_health_bar
     theme_reference/theme_scrolling_container
     theme_reference/theme_selection_list
+    theme_reference/theme_status_bar
     theme_reference/theme_text_box
     theme_reference/theme_text_entry_line
     theme_reference/theme_tooltip
diff --git a/docs/source/theme_reference/theme_progress_bar.rst b/docs/source/theme_reference/theme_progress_bar.rst
new file mode 100644
index 00000000..63e84cb0
--- /dev/null
+++ b/docs/source/theme_reference/theme_progress_bar.rst
@@ -0,0 +1,105 @@
+.. _screen-space-health-bar:
+
+UIProgressBar Theming Parameters
+=========================================
+
+.. raw:: html
+
+    <video width="240" height="44" nocontrols playsinline autoplay muted loop>
+        <source src="../_static/screen_space_health_bar.mp4" type="video/mp4">
+        Your browser does not support the video tag.
+    </video>
+
+The :class:`UIProgressBar <pygame_gui.elements.UIProgressBar>` theming block id is 'progress_bar'.
+
+Colours
+-------
+
+.. figure:: ../_static/screen_space_health_bar_colour_parameters.png
+
+   A diagram of which part of the element is themed by which colour parameter. The text shadow **cannot** be themed
+   with a colour gradient.
+
+:class:`UIProgressBar <pygame_gui.elements.UIProgressBar>` makes use of these colour parameters in a 'colours' block. Most of these colours can
+also be a colour gradient:
+
+ - "**normal_text**" - The colour/gradient of the health bars's text.
+ - "**text_shadow**" - The colour of the shadow behind the text (so it stands out better).
+ - "**normal_border**" - The colour/gradient of the border around the health bar.
+ - "**filled_bar**" - The colour/gradient of the actual bar itself, of the portion of it that is still full.
+ - "**unfilled_bar**" - The colour/gradient of an empty portion of the health bar.
+
+Misc
+----
+
+:class:`UIProgressBar <pygame_gui.elements.UIProgressBar>` accepts the following miscellaneous parameters in a 'misc' block:
+
+ - "**shape**" - Can be one of 'rectangle' or 'rounded_rectangle'. Different shapes for this UI element.
+ - "**shape_corner_radius**" - Only used if our shape is 'rounded_rectangle'. It sets the radius used for the rounded corners.
+ - "**border_width**" - the width in pixels of the border around the bar. Defaults to 1.
+ - "**shadow_width**" - the width in pixels of the shadow behind the bar. Defaults to 1.
+
+
+Font
+-----
+
+:class:`UIProgressBar <pygame_gui.elements.UIProgressBar>` accepts a font specified in the theme via a 'font' block. A 'font' block has these parameters:
+
+ - "**name**" - Necessary to make a valid block. This is the name that this font goes by in the UI, if this is a new font then subsequent font instances with different styles or sizes should use the same name.
+ - "**size**" - Necessary to make a valid block. This is the point size of the font to use on the health bar.
+ - "**bold**" - Optional parameter. Set it to "1" to make this font bold.
+ - "**italic**" - Optional parameter. Set it to "1" to make this font italic.
+
+There are two methods to refer to font resource locations. First, using packaged resources:
+
+ - "**regular_resource** - The location of this font's file with no particular style applied.
+    - **package** - The name of the python package containing this resource - e.g. 'data.fonts'
+    - **resource** - The file name of the resource - e.g. 'FiraCode-Regular.ttf'
+ - "**bold_resource**" - The location of this font's file with bold style applied.
+    - **package** - The name of the python package containing this resource - e.g. 'data.fonts'
+    - **resource** - The file name of the resource - e.g. 'FiraCode-Bold.ttf'
+ - "**italic_resource**" - The location of this font's file with italic style applied.
+    - **package** - The name of the python package containing this resource - e.g. 'data.fonts'
+    - **resource** - The file name of the resource - e.g. 'FiraMono-Italic.ttf'
+ - "**bold_italic_resource**" - The location of this font's file with bold and italic style applied.
+    - **package** - The name of the python package containing this resource - e.g. 'data.fonts'
+    - **resource** - The file name of the resource - e.g. 'FiraMono-BoldItalic.ttf'
+
+Second using paths:
+
+ - "**regular_path**" - The path to this font's file with no particular style applied.
+ - "**bold_path**" - The path to this font's file with bold style applied.
+ - "**italic_path**" - The path to this font's file with italic style applied.
+ - "**bold_italic_path**" - The path to this font's file with bold and italic style applied.
+
+You only need to specify locations if this is the first use of this font name in the GUI.
+
+Example
+-------
+
+Here is an example of a progress bar block in a JSON theme file using the parameters described above.
+
+.. code-block:: json
+   :caption: progress_bar.json
+   :linenos:
+
+    {
+        "progress_bar":
+        {
+            "colours":
+            {
+                "normal_text": "#c5cbd8",
+                "text_shadow": "#777777",
+                "normal_border": "#DDDDDD",
+                "filled_bar": "#f4251b,#A4150b,180",
+                "unfilled_bar": "#CCCCCC"
+            },
+            "font":
+            {
+                "name": "montserrat",
+                "size": "12",
+                "bold": "0",
+                "italic": "1"
+            }
+        }
+    }
diff --git a/docs/source/theme_reference/theme_status_bar.rst b/docs/source/theme_reference/theme_status_bar.rst
new file mode 100644
index 00000000..05de9bcc
--- /dev/null
+++ b/docs/source/theme_reference/theme_status_bar.rst
@@ -0,0 +1,56 @@
+.. _status-bar:
+
+UIStatusBar Theming Parameters
+=========================================
+
+The :class:`UIStatusBar <pygame_gui.elements.UIStatusBar>` theming block id is 'status_bar'.
+
+Colours
+-------
+
+.. figure:: ../_static/world_space_health_bar_colour_parameters.png
+
+   A diagram of which part of the element is themed by which colour parameter.
+
+:class:`UIStatusBar <pygame_gui.elements.UIStatusBar>` makes use of these colour parameters in a 'colours' block:
+
+ - "**normal_border**" - The colour/gradient of the health bar's border if it has one.
+ - "**filled_bar**" - The colour/gradient of the actual bar itself, of the portion of it that is still full.
+ - "**unfilled_bar**" - The colour/gradient of an empty portion of the health bar.
+
+Misc
+-----
+
+:class:`UIStatusBar <pygame_gui.elements.UIStatusBar>` has the following miscellaneous parameters in a 'misc' block:
+
+ - "**shape**" - Can be one of 'rectangle' or 'rounded_rectangle'. Different shapes for this UI element.
+ - "**shape_corner_radius**" - Only used if our shape is 'rounded_rectangle'. It sets the radius used for the rounded corners.
+ - "**follow_sprite_offset**" - The x,y offset values for when the bar follows a sprite. Defaults to "0,0".
+ - "**border_width**" - The width of the border around the health bar. Defaults to "1". Can be "0" to remove the border.
+ - "**shadow_width**" - The width of the border around the health bar. Defaults to "1". Can be "0" to remove the border.
+
+Example
+-------
+
+Here is an example of a status bar block in a JSON theme file using the parameters described above.
+
+.. code-block:: json
+   :caption: status_bar.json
+   :linenos:
+
+    {
+        "status_bar":
+        {
+            "colours":
+            {
+                "normal_border": "#AAAAAA",
+                "filled_bar": "#f4251b",
+                "unfilled_bar": "#CCCCCC"
+            },
+            "misc":
+            {
+                "follow_sprite_offset": "-5, 32",
+                "border_width": "0"
+            }
+        }
+    }
diff --git a/docs/source/theme_reference/theme_world_space_health_bar.rst b/docs/source/theme_reference/theme_world_space_health_bar.rst
index 01ef7b40..d819b620 100644
--- a/docs/source/theme_reference/theme_world_space_health_bar.rst
+++ b/docs/source/theme_reference/theme_world_space_health_bar.rst
@@ -25,7 +25,7 @@ Misc
 
  - "**shape**" - Can be one of 'rectangle' or 'rounded_rectangle'. Different shapes for this UI element.
  - "**shape_corner_radius**" - Only used if our shape is 'rounded_rectangle'. It sets the radius used for the rounded corners.
- - "**hover_height**" - The height in pixels that the health bar will appear over the sprite. Defaults to "10".
+ - "**follow_sprite_offset**" - The x,y offset values the bar relative to the sprite. Defaults to "0,0".
  - "**border_width**" - The width of the border around the health bar. Defaults to "1". Can be "0" to remove the border.
  - "**shadow_width**" - The width of the border around the health bar. Defaults to "1". Can be "0" to remove the border.
 
@@ -49,7 +49,7 @@ Here is an example of a world space health bar block in a JSON theme file using
             },
             "misc":
             {
-                "hover_height": "5",
+                "follow_sprite_offset": "-5, 32",
                 "border_width": "0"
             }
         }
diff --git a/pygame_gui/core/ui_element.py b/pygame_gui/core/ui_element.py
index 178761b6..937f0aa8 100644
--- a/pygame_gui/core/ui_element.py
+++ b/pygame_gui/core/ui_element.py
@@ -935,3 +935,9 @@ def get_anchor_targets(self) -> list:
             targets.append(self.anchors['bottom_target'])
 
         return targets
+
+    @staticmethod
+    def tuple_extract(str_data: str) -> Tuple[int, int]:
+        # Used for parsing coordinate tuples in themes.
+        x, y = str_data.split(',')
+        return int(x), int(y)
diff --git a/pygame_gui/elements/__init__.py b/pygame_gui/elements/__init__.py
index c0347839..28b622c6 100644
--- a/pygame_gui/elements/__init__.py
+++ b/pygame_gui/elements/__init__.py
@@ -12,6 +12,7 @@
 from pygame_gui.elements.ui_text_entry_line import UITextEntryLine
 from pygame_gui.elements.ui_tool_tip import UITooltip
 from pygame_gui.elements.ui_drop_down_menu import UIDropDownMenu
+from pygame_gui.elements.ui_status_bar import UIStatusBar
 from pygame_gui.elements.ui_world_space_health_bar import UIWorldSpaceHealthBar
 from pygame_gui.elements.ui_window import UIWindow
 from pygame_gui.elements.ui_scrolling_container import UIScrollingContainer
diff --git a/pygame_gui/elements/ui_button.py b/pygame_gui/elements/ui_button.py
index e81c6f78..31e26e63 100644
--- a/pygame_gui/elements/ui_button.py
+++ b/pygame_gui/elements/ui_button.py
@@ -587,12 +587,9 @@ def rebuild_from_changed_theme_data(self):
                                                casting_func=int):
             has_any_changed = True
 
-        def tuple_extract(str_data: str) -> Tuple[int, int]:
-            return int(str_data.split(',')[0]), int(str_data.split(',')[1])
-
         if self._check_misc_theme_data_changed(attribute_name='text_shadow_offset',
                                                default_value=(0, 0),
-                                               casting_func=tuple_extract):
+                                               casting_func=self.tuple_extract):
             has_any_changed = True
 
         try:
diff --git a/pygame_gui/elements/ui_label.py b/pygame_gui/elements/ui_label.py
index 2fae9c3b..3581c518 100644
--- a/pygame_gui/elements/ui_label.py
+++ b/pygame_gui/elements/ui_label.py
@@ -203,12 +203,9 @@ def rebuild_from_changed_theme_data(self):
                                                casting_func=int):
             any_changed = True
 
-        def tuple_extract(str_data: str) -> Tuple[int, int]:
-            return int(str_data.split(',')[0]), int(str_data.split(',')[1])
-
         if self._check_misc_theme_data_changed(attribute_name='text_shadow_offset',
                                                default_value=(0, 0),
-                                               casting_func=tuple_extract):
+                                               casting_func=self.tuple_extract):
             any_changed = True
 
         if self._check_text_alignment_theming():
diff --git a/pygame_gui/elements/ui_progress_bar.py b/pygame_gui/elements/ui_progress_bar.py
index 0de591b4..5398cb93 100644
--- a/pygame_gui/elements/ui_progress_bar.py
+++ b/pygame_gui/elements/ui_progress_bar.py
@@ -5,10 +5,10 @@
 from pygame_gui.core import ObjectID
 from pygame_gui.core.interfaces import IContainerLikeInterface, IUIManagerInterface
 from pygame_gui.core import UIElement
-from pygame_gui.core.drawable_shapes import RectDrawableShape, RoundedRectangleShape
+from pygame_gui.elements.ui_status_bar import UIStatusBar
 
 
-class UIProgressBar(UIElement):
+class UIProgressBar(UIStatusBar):
     """
     A UI that will display a progress bar from 0 to 100%
 
@@ -22,6 +22,8 @@ class UIProgressBar(UIElement):
     :param visible: Whether the element is visible by default. Warning - container visibility
                     may override this.
     """
+    element_id = 'progress_bar'
+
     def __init__(self,
                  relative_rect: pygame.Rect,
                  manager: IUIManagerInterface,
@@ -31,182 +33,28 @@ def __init__(self,
                  anchors: Dict[str, str] = None,
                  visible: int = 1):
 
-        super().__init__(relative_rect, manager, container,
-                         starting_height=1,
-                         layer_thickness=1,
-                         anchors=anchors,
-                         visible=visible)
-
-        self._create_valid_ids(container=container,
-                               parent_element=parent_element,
-                               object_id=object_id,
-                               element_id='progress_bar')
-
         self.current_progress = 0.0
         self.maximum_progress = 100.0
-        self.progress_percentage = self.current_progress / self.maximum_progress
-
-        self.font = None
-        self.border_width = None
-        self.shadow_width = None
-        self.border_colour = None
-        self.bar_unfilled_colour = None
-        self.bar_filled_colour = None
-        self.text_shadow_colour = None
-        self.text_colour = None
-        self.text_horiz_alignment = 'center'
-        self.text_vert_alignment = 'center'
-        self.text_horiz_alignment_padding = 1
-        self.text_vert_alignment_padding = 1
-
-        self.border_rect = None
-        self.capacity_width = None
-        self.capacity_height = None
-        self.capacity_rect = None
-        self.current_progress_rect = None
-
-        self.drawable_shape = None
-        self.shape = 'rectangle'
-        self.shape_corner_radius = None
-
-        self.set_image(None)
-        self.background_text = None
-        self.foreground_text = None
-
-        self.rebuild_from_changed_theme_data()
-
-    def rebuild(self):
-        """
-        Rebuild the progress bar entirely because the theming data has changed.
-
-        """
-        border_rect_width = self.rect.width - (self.shadow_width * 2)
-        border_rect_height = self.rect.height - (self.shadow_width * 2)
-        self.border_rect = pygame.Rect((self.shadow_width,
-                                        self.shadow_width),
-                                       (border_rect_width, border_rect_height))
-
-        self.capacity_width = self.rect.width - (self.shadow_width * 2) - self.border_width * 2
-        self.capacity_height = self.rect.height - (self.shadow_width * 2) - self.border_width * 2
-        self.capacity_rect = pygame.Rect((self.shadow_width + self.border_width,
-                                          self.shadow_width + self.border_width),
-                                         (self.capacity_width, self.capacity_height))
-
-        self.current_progress_rect = pygame.Rect((self.shadow_width + self.border_width,
-                                                  self.shadow_width + self.border_width),
-                                                 (int(self.capacity_width *
-                                                      self.progress_percentage),
-                                                  self.capacity_height))
 
-        self.redraw()
-
-    def redraw(self):
-        """
-        Redraws the progress bar rectangles and text onto the underlying sprite's image surface.
-        Takes a little while so we only do it when the progress has changed.
-        """
-        progress_string = f"{self.current_progress:0.1f}/{self.maximum_progress:0.1f}"
-
-        theming_parameters = {'normal_bg': self.bar_unfilled_colour,
-                              'normal_border': self.border_colour,
-                              'border_width': self.border_width,
-                              'shadow_width': self.shadow_width,
-                              'shape_corner_radius': self.shape_corner_radius,
-                              'filled_bar': self.bar_filled_colour,
-                              'filled_bar_width_percentage': self.progress_percentage,
-                              'font': self.font,
-                              'text': progress_string,
-                              'normal_text': self.text_colour,
-                              'normal_text_shadow': self.text_shadow_colour,
-                              'text_shadow': (1,
-                                              0,
-                                              0,
-                                              self.text_shadow_colour,
-                                              False),
-                              'text_horiz_alignment': self.text_horiz_alignment,
-                              'text_vert_alignment': self.text_vert_alignment,
-                              'text_horiz_alignment_padding': self.text_horiz_alignment_padding,
-                              'text_vert_alignment_padding': self.text_vert_alignment_padding,
-                              }
+        super().__init__(relative_rect=relative_rect,
+                         manager=manager,
+                         container=container,
+                         parent_element=parent_element,
+                         object_id=object_id,
+                         anchors=anchors,
+                         visible=visible)
 
-        if self.shape == 'rectangle':
-            self.drawable_shape = RectDrawableShape(self.rect, theming_parameters,
-                                                    ['normal'], self.ui_manager)
-        elif self.shape == 'rounded_rectangle':
-            self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters,
-                                                        ['normal'], self.ui_manager)
+    @property
+    def progress_percentage(self):
+        return self.current_progress / self.maximum_progress
 
-        self.set_image(self.drawable_shape.get_fresh_surface())
+    def status_text(self):
+        """ Subclass and override this method to change what text is displayed, or to suppress the text. """
+        return f"{self.current_progress:0.1f}/{self.maximum_progress:0.1f}"
 
     def set_current_progress(self, progress: float):
-        """
-        Set the current progress of the bar.
-
-        :param progress: The level of progress to set from 0 to 100.0
-        """
-        if progress != self.current_progress:
-            progress = min(progress, self.maximum_progress)
-            self.current_progress = progress
-            self.progress_percentage = self.current_progress / self.maximum_progress
-            rect_width = int(self.capacity_width * self.progress_percentage)
-            self.current_progress_rect = pygame.Rect((self.shadow_width + self.border_width,
-                                                      self.shadow_width + self.border_width),
-                                                     (rect_width,
-                                                      self.capacity_height))
-            self.redraw()
-
-    def rebuild_from_changed_theme_data(self):
-        """
-        Called by the UIManager to check the theming data and rebuild whatever needs rebuilding
-        for this element when the theme data has changed.
-        """
-        super().rebuild_from_changed_theme_data()
-        has_any_changed = False
-
-        font = self.ui_theme.get_font(self.combined_element_ids)
-        if font != self.font:
-            self.font = font
-            has_any_changed = True
-
-        if self._check_misc_theme_data_changed(attribute_name='shape',
-                                               default_value='rectangle',
-                                               casting_func=str,
-                                               allowed_values=['rectangle',
-                                                               'rounded_rectangle']):
-            has_any_changed = True
-
-        if self._check_shape_theming_changed(defaults={'border_width': 1,
-                                                       'shadow_width': 2,
-                                                       'shape_corner_radius': 2}):
-            has_any_changed = True
-
-        border_colour = self.ui_theme.get_colour_or_gradient('normal_border',
-                                                             self.combined_element_ids)
-        if border_colour != self.border_colour:
-            self.border_colour = border_colour
-            has_any_changed = True
-
-        bar_unfilled_colour = self.ui_theme.get_colour_or_gradient('unfilled_bar',
-                                                                   self.combined_element_ids)
-        if bar_unfilled_colour != self.bar_unfilled_colour:
-            self.bar_unfilled_colour = bar_unfilled_colour
-            has_any_changed = True
-
-        bar_filled_colour = self.ui_theme.get_colour_or_gradient('filled_bar',
-                                                                 self.combined_element_ids)
-        if bar_filled_colour != self.bar_filled_colour:
-            self.bar_filled_colour = bar_filled_colour
-            has_any_changed = True
-
-        text_shadow_colour = self.ui_theme.get_colour('text_shadow', self.combined_element_ids)
-        if text_shadow_colour != self.text_shadow_colour:
-            self.text_shadow_colour = text_shadow_colour
-            has_any_changed = True
-
-        text_colour = self.ui_theme.get_colour_or_gradient('normal_text', self.combined_element_ids)
-        if text_colour != self.text_colour:
-            self.text_colour = text_colour
-            has_any_changed = True
+        # Now that we subclass UIStatusBar, set_current_progress() and self.current_progress are mostly here for backward compatibility.
+        self.current_progress = progress
 
-        if has_any_changed:
-            self.rebuild()
+        # Setting this triggers updating if necessary.
+        self.percent_full = progress
diff --git a/pygame_gui/elements/ui_screen_space_health_bar.py b/pygame_gui/elements/ui_screen_space_health_bar.py
index 2409fc6f..31e8b735 100644
--- a/pygame_gui/elements/ui_screen_space_health_bar.py
+++ b/pygame_gui/elements/ui_screen_space_health_bar.py
@@ -5,10 +5,10 @@
 from pygame_gui.core import ObjectID
 from pygame_gui.core.interfaces import IContainerLikeInterface, IUIManagerInterface
 from pygame_gui.core import UIElement
-from pygame_gui.core.drawable_shapes import RectDrawableShape, RoundedRectangleShape
+from pygame_gui.elements.ui_status_bar import UIStatusBar
 
 
-class UIScreenSpaceHealthBar(UIElement):
+class UIScreenSpaceHealthBar(UIStatusBar):
     """
     A UI that will display health capacity and current health for a sprite in 'screen space'.
     That means it won't move with the camera. This is a good choice for a user/player sprite.
@@ -24,6 +24,8 @@ class UIScreenSpaceHealthBar(UIElement):
     :param visible: Whether the element is visible by default. Warning - container visibility
                     may override this.
     """
+    element_id = 'screen_space_health_bar'
+
     def __init__(self,
                  relative_rect: pygame.Rect,
                  manager: IUIManagerInterface,
@@ -34,209 +36,52 @@ def __init__(self,
                  anchors: Dict[str, str] = None,
                  visible: int = 1):
 
-        super().__init__(relative_rect, manager, container,
-                         starting_height=1,
-                         layer_thickness=1,
+        # Setting this here because UIProgressBar doesn't accept a percent_method in __init__.
+        self.percent_method = self.health_percent
+
+        # Set this using the property, for error handling.
+        self.set_sprite_to_monitor(sprite_to_monitor)
+
+        super().__init__(relative_rect=relative_rect,
+                         manager=manager,
+                         sprite=sprite_to_monitor,
+                         follow_sprite=False,
+                         percent_method=self.health_percent,
+                         container=container,
+                         parent_element=parent_element,
+                         object_id=object_id,
                          anchors=anchors,
                          visible=visible)
 
-        self._create_valid_ids(container=container,
-                               parent_element=parent_element,
-                               object_id=object_id,
-                               element_id='screen_space_health_bar')
-
-        self.current_health = 50
-        self.health_capacity = 100
-        self.health_percentage = self.current_health / self.health_capacity
-
-        self.font = None
-        self.border_width = None
-        self.shadow_width = None
-        self.border_colour = None
-        self.bar_unfilled_colour = None
-        self.bar_filled_colour = None
-        self.text_shadow_colour = None
-        self.text_colour = None
-        self.text_horiz_alignment = 'center'
-        self.text_vert_alignment = 'center'
-        self.text_horiz_alignment_padding = 1
-        self.text_vert_alignment_padding = 1
-
-        self.border_rect = None
-        self.capacity_width = None
-        self.capacity_height = None
-        self.capacity_rect = None
-        self.current_health_rect = None
-
-        self.drawable_shape = None
-        self.shape = 'rectangle'
-        self.shape_corner_radius = None
-
-        if sprite_to_monitor is not None:
+    @property
+    def current_health(self):
+        if not self.sprite:
+            return 50
+        return self.sprite.current_health
+
+    @property
+    def health_capacity(self):
+        if not self.sprite:
+            return 100
+        return self.sprite.health_capacity
+
+    def health_percent(self):
+        return self.current_health / self.health_capacity
+
+    @property
+    def health_percentage(self):
+        # Now that we subclass UIStatusBar, this is here for backward compatibility.
+        return self.health_percent()
+
+    def set_sprite_to_monitor(self, sprite_to_monitor: pygame.sprite.Sprite):
+        if sprite_to_monitor:
             if not hasattr(sprite_to_monitor, 'health_capacity'):
                 raise AttributeError('Sprite does not have health_capacity attribute')
             if not hasattr(sprite_to_monitor, 'current_health'):
                 raise AttributeError('Sprite does not have current_health attribute')
-            self.sprite_to_monitor = sprite_to_monitor
-        else:
-            self.sprite_to_monitor = None
-        self.set_image(None)
-        self.background_text = None
-        self.foreground_text = None
+        self.sprite = sprite_to_monitor
 
-        self.rebuild_from_changed_theme_data()
+    def status_text(self):
+        """ Subclass and override this method to change what text is displayed, or to suppress the text. """
+        return f"{int(self.current_health)}/{int(self.health_capacity)}"
 
-    def set_sprite_to_monitor(self, sprite_to_monitor: pygame.sprite.Sprite):
-        """
-        Sprite to monitor the health of. Must have 'health_capacity' and 'current_health'
-        attributes.
-
-        :param sprite_to_monitor:
-
-        """
-        if not hasattr(sprite_to_monitor, 'health_capacity'):
-            raise AttributeError('Sprite does not have health_capacity attribute')
-        if not hasattr(sprite_to_monitor, 'current_health'):
-            raise AttributeError('Sprite does not have current_health attribute')
-        self.sprite_to_monitor = sprite_to_monitor
-
-    def rebuild(self):
-        """
-        Rebuild the health bar entirely because the theming data has changed.
-
-        """
-        border_rect_width = self.rect.width - (self.shadow_width * 2)
-        border_rect_height = self.rect.height - (self.shadow_width * 2)
-        self.border_rect = pygame.Rect((self.shadow_width,
-                                        self.shadow_width),
-                                       (border_rect_width, border_rect_height))
-
-        self.capacity_width = self.rect.width - (self.shadow_width * 2) - self.border_width * 2
-        self.capacity_height = self.rect.height - (self.shadow_width * 2) - self.border_width * 2
-        self.capacity_rect = pygame.Rect((self.shadow_width + self.border_width,
-                                          self.shadow_width + self.border_width),
-                                         (self.capacity_width, self.capacity_height))
-
-        self.current_health_rect = pygame.Rect((self.shadow_width + self.border_width,
-                                                self.shadow_width + self.border_width),
-                                               (int(self.capacity_width * self.health_percentage),
-                                                self.capacity_height))
-
-        self.redraw()
-
-    def redraw(self):
-        """
-        Redraws the health bar rectangles and text onto the underlying sprite's image surface.
-        Takes a little while so we only do it when the health has changed.
-        """
-        health_display_string = str(self.current_health) + "/" + str(self.health_capacity)
-
-        theming_parameters = {'normal_bg': self.bar_unfilled_colour,
-                              'normal_border': self.border_colour,
-                              'border_width': self.border_width,
-                              'shadow_width': self.shadow_width,
-                              'shape_corner_radius': self.shape_corner_radius,
-                              'filled_bar': self.bar_filled_colour,
-                              'filled_bar_width_percentage': self.health_percentage,
-                              'font': self.font,
-                              'text': health_display_string,
-                              'normal_text': self.text_colour,
-                              'normal_text_shadow': self.text_shadow_colour,
-                              'text_shadow': (1,
-                                              0,
-                                              0,
-                                              self.text_shadow_colour,
-                                              False),
-                              'text_horiz_alignment': self.text_horiz_alignment,
-                              'text_vert_alignment': self.text_vert_alignment,
-                              'text_horiz_alignment_padding': self.text_horiz_alignment_padding,
-                              'text_vert_alignment_padding': self.text_vert_alignment_padding,
-                              }
-
-        if self.shape == 'rectangle':
-            self.drawable_shape = RectDrawableShape(self.rect, theming_parameters,
-                                                    ['normal'], self.ui_manager)
-        elif self.shape == 'rounded_rectangle':
-            self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters,
-                                                        ['normal'], self.ui_manager)
-
-        self.set_image(self.drawable_shape.get_fresh_surface())
-
-    def update(self, time_delta: float):
-        """
-        Updates the health bar sprite's image with the latest health data from the
-        sprite we are monitoring. Only triggers a rebuild if the health values have changed.
-
-        :param time_delta: time passed in seconds between one call to this method and the next.
-
-        """
-        super().update(time_delta)
-        if (self.alive() and self.sprite_to_monitor is not None and
-                (self.sprite_to_monitor.health_capacity != self.health_capacity or
-                 self.current_health != self.sprite_to_monitor.current_health)):
-            self.current_health = self.sprite_to_monitor.current_health
-            self.health_capacity = self.sprite_to_monitor.health_capacity
-            self.health_percentage = self.current_health / self.health_capacity
-
-            rect_width = int(self.capacity_width * self.health_percentage)
-            self.current_health_rect = pygame.Rect((self.shadow_width + self.border_width,
-                                                    self.shadow_width + self.border_width),
-                                                   (rect_width,
-                                                    self.capacity_height))
-            self.redraw()
-
-    def rebuild_from_changed_theme_data(self):
-        """
-        Called by the UIManager to check the theming data and rebuild whatever needs rebuilding
-        for this element when the theme data has changed.
-        """
-        super().rebuild_from_changed_theme_data()
-        has_any_changed = False
-
-        font = self.ui_theme.get_font(self.combined_element_ids)
-        if font != self.font:
-            self.font = font
-            has_any_changed = True
-
-        if self._check_misc_theme_data_changed(attribute_name='shape',
-                                               default_value='rectangle',
-                                               casting_func=str,
-                                               allowed_values=['rectangle',
-                                                               'rounded_rectangle']):
-            has_any_changed = True
-
-        if self._check_shape_theming_changed(defaults={'border_width': 1,
-                                                       'shadow_width': 2,
-                                                       'shape_corner_radius': 2}):
-            has_any_changed = True
-
-        border_colour = self.ui_theme.get_colour_or_gradient('normal_border',
-                                                             self.combined_element_ids)
-        if border_colour != self.border_colour:
-            self.border_colour = border_colour
-            has_any_changed = True
-
-        bar_unfilled_colour = self.ui_theme.get_colour_or_gradient('unfilled_bar',
-                                                                   self.combined_element_ids)
-        if bar_unfilled_colour != self.bar_unfilled_colour:
-            self.bar_unfilled_colour = bar_unfilled_colour
-            has_any_changed = True
-
-        bar_filled_colour = self.ui_theme.get_colour_or_gradient('filled_bar',
-                                                                 self.combined_element_ids)
-        if bar_filled_colour != self.bar_filled_colour:
-            self.bar_filled_colour = bar_filled_colour
-            has_any_changed = True
-
-        text_shadow_colour = self.ui_theme.get_colour('text_shadow', self.combined_element_ids)
-        if text_shadow_colour != self.text_shadow_colour:
-            self.text_shadow_colour = text_shadow_colour
-            has_any_changed = True
-
-        text_colour = self.ui_theme.get_colour_or_gradient('normal_text', self.combined_element_ids)
-        if text_colour != self.text_colour:
-            self.text_colour = text_colour
-            has_any_changed = True
-
-        if has_any_changed:
-            self.rebuild()
diff --git a/pygame_gui/elements/ui_status_bar.py b/pygame_gui/elements/ui_status_bar.py
new file mode 100644
index 00000000..d671b3f8
--- /dev/null
+++ b/pygame_gui/elements/ui_status_bar.py
@@ -0,0 +1,278 @@
+from typing import Union, Dict, Callable
+
+import pygame
+
+from pygame_gui.core import ObjectID
+from pygame_gui.core.interfaces import IContainerLikeInterface, IUIManagerInterface
+from pygame_gui.core import UIElement
+from pygame_gui.core.drawable_shapes import RectDrawableShape, RoundedRectangleShape
+
+
+class UIStatusBar(UIElement):
+    """
+    Displays a status/progress bar.
+
+    This is a flexible class that can be used to display status for a sprite (health/mana/fatigue, etc),
+    or to provide a status bar on the screen not attached to any particular object. You can use multiple
+    status bars for a sprite to show different status items if desired.
+
+    You can use the percent_full attribute to manually set the status, or you can provide a pointer to a method
+    that will provide the percentage information.
+
+    This is a kitchen sink class with several ways to use it; you may want to look at the subclasses built on top
+    of it that are designed to be simpler to use, such as UIProgressBar, UIWorldSpaceHealthBar, and
+    UIScreenSpaceHealthBar.
+
+    :param relative_rect: The rectangle that defines the size of the health bar.
+    :param sprite: Optional sprite to monitor for status info, and for drawing the bar with the sprite.
+    :param follow_sprite: If there's a sprite, this indicates whether the bar should be drawn at the sprite's location.
+    :param percent_method: Optional method signature to call to get the percent complete. (To provide a method signature,
+                           simply reference the method without parenthesis, such as self.health_percent.)
+    :param manager: The UIManager that manages this element.
+    :param container: The container that this element is within. If set to None will be the root window's container.
+    :param parent_element: The element this element 'belongs to' in the theming hierarchy.
+    :param object_id: A custom defined ID for fine tuning of theming.
+    :param anchors: A dictionary describing what this element's relative_rect is relative to.
+    :param visible: Whether the element is visible by default. Warning - container visibility
+                    may override this.
+
+    """
+
+    element_id = 'status_bar'
+
+    def __init__(self,
+                 relative_rect: pygame.Rect,
+                 manager: IUIManagerInterface,
+                 sprite: Union[pygame.sprite.Sprite, None] = None,
+                 follow_sprite: bool = True,
+                 percent_method: Union[Callable[[], float], None] = None,
+                 container: Union[IContainerLikeInterface, None] = None,
+                 parent_element: UIElement = None,
+                 object_id: Union[ObjectID, str, None] = None,
+                 anchors: Dict[str, str] = None,
+                 visible: int = 1):
+
+        super().__init__(relative_rect, manager, container,
+                         starting_height=1,
+                         layer_thickness=1,
+                         anchors=anchors,
+                         visible=visible)
+
+        self._create_valid_ids(container=container,
+                               parent_element=parent_element,
+                               object_id=object_id,
+                               element_id=self.element_id)
+
+        self.sprite = sprite
+        self.follow_sprite = follow_sprite
+        self.follow_sprite_offset = (0, 0)
+
+        self.percent_method = percent_method
+        self._percent_full = 0
+        self.status_changed = False
+
+        self.border_colour = None
+        self.bar_filled_colour = None
+        self.bar_unfilled_colour = None
+        self.hover_height = None
+        self.border_width = None
+        self.shadow_width = None
+        self.border_rect = None
+        self.capacity_width = None
+        self.capacity_height = None
+        self.capacity_rect = None
+        self.current_status_rect = None
+
+        self.drawable_shape = None
+        self.shape = 'rectangle'
+        self.shape_corner_radius = None
+
+        self.font = None
+        self.text_shadow_colour = None
+        self.text_colour = None
+        self.text_horiz_alignment = 'center'
+        self.text_vert_alignment = 'center'
+        self.text_horiz_alignment_padding = 1
+        self.text_vert_alignment_padding = 1
+        self.background_text = None
+        self.foreground_text = None
+
+        self.set_image(None)
+
+        self.rebuild_from_changed_theme_data()
+
+    @property
+    def percent_full(self):
+        """ Use this property to directly change the status bar. """
+        return self._percent_full
+
+    @percent_full.setter
+    def percent_full(self, value):
+        # We need a decimal percentage
+        if value > 1:
+            value = value / 100
+        if value != self._percent_full:
+            self._percent_full = value
+            self.status_changed = True
+
+    @property
+    def position(self):
+        if self.sprite and self.follow_sprite:
+            offset_x = self.sprite.rect.x + self.follow_sprite_offset[0]
+            offset_y = self.sprite.rect.y + self.follow_sprite_offset[1] - self.hover_height
+            return offset_x, offset_y
+        else:
+            return self.relative_rect.x, self.relative_rect.y
+
+    def rebuild(self):
+        """
+        Rebuild the status bar entirely because the theming data has changed.
+
+        """
+        self.rect.x, self.rect.y = self.position
+
+        self.border_rect = pygame.Rect((self.shadow_width, self.shadow_width),
+                                       (self.rect.width - (self.shadow_width * 2),
+                                        self.rect.height - (self.shadow_width * 2)))
+
+        self.capacity_width = self.rect.width - (self.shadow_width * 2) - (self.border_width * 2)
+        self.capacity_height = self.rect.height - (self.shadow_width * 2) - (self.border_width * 2)
+        self.capacity_rect = pygame.Rect((self.border_width + self.shadow_width,
+                                          self.border_width + self.shadow_width),
+                                         (self.capacity_width, self.capacity_height))
+
+        self.redraw()
+
+    def update(self, time_delta: float):
+        """
+        Updates the status bar sprite's image and rectangle with the latest status and position
+        data from the sprite we are monitoring
+
+        :param time_delta: time passed in seconds between one call to this method and the next.
+
+        """
+        super().update(time_delta)
+        if self.alive():
+            self.rect.x, self.rect.y = self.position
+            self.relative_rect.topleft = self.rect.topleft
+
+            # If they've provided a method to call, we'll track previous value in percent_full.
+            if self.percent_method:
+                # This triggers status_changed if necessary.
+                self.percent_full = self.percent_method()
+
+            if self.status_changed:
+                self.status_changed = False
+                self.redraw()
+
+    def status_text(self):
+        """ To display text in the bar, subclass UIStatusBar and override this method. """
+        return None
+
+    def redraw(self):
+        """
+        Redraw the status bar when something, other than it's position has changed.
+
+        """
+        theming_parameters = {'normal_bg': self.bar_unfilled_colour,
+                              'normal_border': self.border_colour,
+                              'border_width': self.border_width,
+                              'shadow_width': self.shadow_width,
+                              'shape_corner_radius': self.shape_corner_radius,
+                              'filled_bar': self.bar_filled_colour,
+                              'filled_bar_width_percentage': self.percent_full,
+                              'follow_sprite_offset': self.follow_sprite_offset}
+
+        text = self.status_text()
+        if text:
+            text_parameters = {'font': self.font,
+                               'text': text,
+                               'normal_text': self.text_colour,
+                               'normal_text_shadow': self.text_shadow_colour,
+                               'text_shadow': (1,
+                                               0,
+                                               0,
+                                               self.text_shadow_colour,
+                                               False),
+                               'text_horiz_alignment': self.text_horiz_alignment,
+                               'text_vert_alignment': self.text_vert_alignment,
+                               'text_horiz_alignment_padding': self.text_horiz_alignment_padding,
+                               'text_vert_alignment_padding': self.text_vert_alignment_padding,
+                               }
+            theming_parameters.update(text_parameters)
+
+        if self.shape == 'rectangle':
+            self.drawable_shape = RectDrawableShape(self.rect, theming_parameters,
+                                                    ['normal'], self.ui_manager)
+        elif self.shape == 'rounded_rectangle':
+            self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters,
+                                                        ['normal'], self.ui_manager)
+
+        self.set_image(self.drawable_shape.get_fresh_surface())
+
+    def rebuild_from_changed_theme_data(self):
+        """
+        Called by the UIManager to check the theming data and rebuild whatever needs rebuilding
+        for this element when the theme data has changed.
+
+        """
+        has_any_changed = False
+
+        if self._check_misc_theme_data_changed(attribute_name='follow_sprite_offset',
+                                               default_value=(0, 0),
+                                               casting_func=self.tuple_extract):
+            has_any_changed = True
+
+        if self._check_misc_theme_data_changed(attribute_name='shape',
+                                               default_value='rectangle',
+                                               casting_func=str,
+                                               allowed_values=['rectangle',
+                                                               'rounded_rectangle']):
+            has_any_changed = True
+
+        if self._check_shape_theming_changed(defaults={'border_width': 1,
+                                                       'shadow_width': 2,
+                                                       'shape_corner_radius': 2}):
+            has_any_changed = True
+
+        if self._check_misc_theme_data_changed(attribute_name='hover_height',
+                                               default_value=1,
+                                               casting_func=int):
+            has_any_changed = True
+
+        border_colour = self.ui_theme.get_colour_or_gradient('normal_border',
+                                                             self.combined_element_ids)
+        if border_colour != self.border_colour:
+            self.border_colour = border_colour
+            has_any_changed = True
+
+        bar_unfilled_colour = self.ui_theme.get_colour_or_gradient('unfilled_bar',
+                                                                   self.combined_element_ids)
+        if bar_unfilled_colour != self.bar_unfilled_colour:
+            self.bar_unfilled_colour = bar_unfilled_colour
+            has_any_changed = True
+
+        bar_filled_colour = self.ui_theme.get_colour_or_gradient('filled_bar',
+                                                                 self.combined_element_ids)
+        if bar_filled_colour != self.bar_filled_colour:
+            self.bar_filled_colour = bar_filled_colour
+            has_any_changed = True
+
+        if self.status_text:
+            font = self.ui_theme.get_font(self.combined_element_ids)
+            if font != self.font:
+                self.font = font
+                has_any_changed = True
+
+            text_shadow_colour = self.ui_theme.get_colour('text_shadow', self.combined_element_ids)
+            if text_shadow_colour != self.text_shadow_colour:
+                self.text_shadow_colour = text_shadow_colour
+                has_any_changed = True
+
+            text_colour = self.ui_theme.get_colour_or_gradient('normal_text', self.combined_element_ids)
+            if text_colour != self.text_colour:
+                self.text_colour = text_colour
+                has_any_changed = True
+
+        if has_any_changed:
+            self.rebuild()
diff --git a/pygame_gui/elements/ui_text_box.py b/pygame_gui/elements/ui_text_box.py
index 759ffa67..a3b673b8 100644
--- a/pygame_gui/elements/ui_text_box.py
+++ b/pygame_gui/elements/ui_text_box.py
@@ -699,12 +699,9 @@ def rebuild_from_changed_theme_data(self):
                                                        'shape_corner_radius': 2}):
             has_any_changed = True
 
-        def tuple_extract(str_data: str) -> Tuple[int, int]:
-            return int(str_data.split(',')[0]), int(str_data.split(',')[1])
-
         if self._check_misc_theme_data_changed(attribute_name='padding',
                                                default_value=(5, 5),
-                                               casting_func=tuple_extract):
+                                               casting_func=self.tuple_extract):
             has_any_changed = True
 
         # colour parameters
diff --git a/pygame_gui/elements/ui_text_entry_line.py b/pygame_gui/elements/ui_text_entry_line.py
index bc7941ed..bdedc7c6 100644
--- a/pygame_gui/elements/ui_text_entry_line.py
+++ b/pygame_gui/elements/ui_text_entry_line.py
@@ -1,7 +1,7 @@
 import re
 import warnings
 
-from typing import Union, List, Tuple, Dict, Optional
+from typing import Union, List, Dict, Optional
 
 import pygame
 
@@ -861,12 +861,9 @@ def rebuild_from_changed_theme_data(self):
                                                        'shape_corner_radius': 2}):
             has_any_changed = True
 
-        def tuple_extract(str_data: str) -> Tuple[int, int]:
-            return int(str_data.split(',')[0]), int(str_data.split(',')[1])
-
         if self._check_misc_theme_data_changed(attribute_name='padding',
                                                default_value=(2, 2),
-                                               casting_func=tuple_extract):
+                                               casting_func=self.tuple_extract):
             has_any_changed = True
 
         if self._check_theme_colours_changed():
diff --git a/pygame_gui/elements/ui_world_space_health_bar.py b/pygame_gui/elements/ui_world_space_health_bar.py
index 8bf25069..06595458 100644
--- a/pygame_gui/elements/ui_world_space_health_bar.py
+++ b/pygame_gui/elements/ui_world_space_health_bar.py
@@ -5,10 +5,10 @@
 from pygame_gui.core import ObjectID
 from pygame_gui.core.interfaces import IContainerLikeInterface, IUIManagerInterface
 from pygame_gui.core import UIElement
-from pygame_gui.core.drawable_shapes import RectDrawableShape, RoundedRectangleShape
+from pygame_gui.elements.ui_status_bar import UIStatusBar
 
 
-class UIWorldSpaceHealthBar(UIElement):
+class UIWorldSpaceHealthBar(UIStatusBar):
     """
     A UI that will display a sprite's 'health_capacity' and their 'current_health' in 'world space'
     above the sprite. This means that the health bar will move with the camera and the sprite
@@ -41,6 +41,8 @@ def __init__(self, *groups):
             self.health_capacity = 100
             self.rect = pygame.Rect(0, 0, 32, 64)
 
+    element_id = 'world_space_health_bar'
+
     def __init__(self,
                  relative_rect: pygame.Rect,
                  sprite_to_monitor: Union[pygame.sprite.Sprite, ExampleHealthSprite],
@@ -51,17 +53,6 @@ def __init__(self,
                  anchors: Dict[str, str] = None,
                  visible: int = 1):
 
-        super().__init__(relative_rect, manager, container,
-                         starting_height=1,
-                         layer_thickness=1,
-                         anchors=anchors,
-                         visible=visible)
-
-        self._create_valid_ids(container=container,
-                               parent_element=parent_element,
-                               object_id=object_id,
-                               element_id='world_space_health_bar')
-
         if sprite_to_monitor is not None:
             if not hasattr(sprite_to_monitor, 'health_capacity'):
                 raise AttributeError('Sprite does not have health_capacity attribute')
@@ -70,156 +61,26 @@ def __init__(self,
             self.sprite_to_monitor = sprite_to_monitor
         else:
             self.sprite_to_monitor = None
-            raise AssertionError('Need sprite to monitor')
-
-        self.current_health = self.sprite_to_monitor.current_health
-        self.health_capacity = self.sprite_to_monitor.health_capacity
-        self.health_percentage = self.current_health / self.health_capacity
-
-        self.border_colour = None
-        self.health_empty_colour = None
-        self.bar_filled_colour = None
-        self.bar_unfilled_colour = None
-        self.health_colour = None
-        self.hover_height = None
-        self.border_width = None
-        self.shadow_width = None
-        self.position = None
-        self.border_rect = None
-        self.capacity_width = None
-        self.capacity_height = None
-        self.health_capacity_rect = None
-        self.current_health_rect = None
-
-        self.drawable_shape = None
-        self.shape = 'rectangle'
-        self.shape_corner_radius = None
-
-        self.set_image(None)
-
-        self.rebuild_from_changed_theme_data()
-
-    def rebuild(self):
-        """
-        Rebuild the health bar entirely because the theming data has changed.
-
-        """
-
-        self.position = [self.sprite_to_monitor.rect.x,
-                         self.sprite_to_monitor.rect.y - self.hover_height]
-
-        self.rect.x = self.position[0]
-        self.rect.y = self.position[1]
-
-        self.border_rect = pygame.Rect((self.shadow_width, self.shadow_width),
-                                       (self.rect.width - (self.shadow_width * 2),
-                                        self.rect.height - (self.shadow_width * 2)))
-
-        self.capacity_width = self.rect.width - (self.shadow_width * 2) - (self.border_width * 2)
-        self.capacity_height = self.rect.height - (self.shadow_width * 2) - (self.border_width * 2)
-        self.health_capacity_rect = pygame.Rect((self.border_width + self.shadow_width,
-                                                 self.border_width + self.shadow_width),
-                                                (self.capacity_width, self.capacity_height))
-
-        self.current_health_rect = pygame.Rect((self.border_width + self.shadow_width,
-                                                self.border_width + self.shadow_width),
-                                               (int(self.capacity_width * self.health_percentage),
-                                                self.capacity_height))
-
-        self.redraw()
-
-    def update(self, time_delta: float):
-        """
-        Updates the health bar sprite's image and rectangle with the latest health and position
-        data from the sprite we are monitoring
-
-        :param time_delta: time passed in seconds between one call to this method and the next.
-
-        """
-        super().update(time_delta)
-        if self.alive():
-            self.position = [self.sprite_to_monitor.rect.x,
-                             self.sprite_to_monitor.rect.y - self.hover_height]
-
-            self.rect.x = self.position[0]
-            self.rect.y = self.position[1]
-            self.relative_rect.topleft = self.rect.topleft
-
-            if (self.current_health != self.sprite_to_monitor.current_health) or (
-                    self.health_capacity != self.sprite_to_monitor.health_capacity):
+            if self.__class__ == UIWorldSpaceHealthBar:
+                raise AssertionError('Need sprite to monitor')
+
+        super().__init__(relative_rect=relative_rect,
+                         manager=manager,
+                         sprite=sprite_to_monitor,
+                         percent_method=self.health_percent,
+                         container=container,
+                         parent_element=parent_element,
+                         object_id=object_id,
+                         anchors=anchors,
+                         visible=visible)
 
-                self.current_health = self.sprite_to_monitor.current_health
-                self.health_capacity = self.sprite_to_monitor.health_capacity
-                self.health_percentage = self.current_health / self.health_capacity
+    @property
+    def current_health(self):
+        return self.sprite_to_monitor.current_health
 
-                self.redraw()
+    @property
+    def health_capacity(self):
+        return self.sprite_to_monitor.health_capacity
 
-    def redraw(self):
-        """
-        Redraw the health bar when something, other than it's position has changed.
-        """
-        self.current_health_rect.width = int(self.capacity_width * self.health_percentage)
-
-        theming_parameters = {'normal_bg': self.bar_unfilled_colour,
-                              'normal_border': self.border_colour,
-                              'border_width': self.border_width,
-                              'shadow_width': self.shadow_width,
-                              'shape_corner_radius': self.shape_corner_radius,
-                              'filled_bar': self.bar_filled_colour,
-                              'filled_bar_width_percentage': self.health_percentage}
-
-        if self.shape == 'rectangle':
-            self.drawable_shape = RectDrawableShape(self.rect, theming_parameters,
-                                                    ['normal'], self.ui_manager)
-        elif self.shape == 'rounded_rectangle':
-            self.drawable_shape = RoundedRectangleShape(self.rect, theming_parameters,
-                                                        ['normal'], self.ui_manager)
-
-        self.set_image(self.drawable_shape.get_fresh_surface())
-
-    def rebuild_from_changed_theme_data(self):
-        """
-        Called by the UIManager to check the theming data and rebuild whatever needs rebuilding
-        for this element when the theme data has changed.
-        """
-        super().rebuild_from_changed_theme_data()
-
-        has_any_changed = False
-
-        if self._check_misc_theme_data_changed(attribute_name='shape',
-                                               default_value='rectangle',
-                                               casting_func=str,
-                                               allowed_values=['rectangle',
-                                                               'rounded_rectangle']):
-            has_any_changed = True
-
-        if self._check_shape_theming_changed(defaults={'border_width': 1,
-                                                       'shadow_width': 2,
-                                                       'shape_corner_radius': 2}):
-            has_any_changed = True
-
-        if self._check_misc_theme_data_changed(attribute_name='hover_height',
-                                               default_value=1,
-                                               casting_func=int):
-            has_any_changed = True
-
-        border_colour = self.ui_theme.get_colour_or_gradient('normal_border',
-                                                             self.combined_element_ids)
-        if border_colour != self.border_colour:
-            self.border_colour = border_colour
-            has_any_changed = True
-
-        bar_unfilled_colour = self.ui_theme.get_colour_or_gradient('unfilled_bar',
-                                                                   self.combined_element_ids)
-        if bar_unfilled_colour != self.bar_unfilled_colour:
-            self.bar_unfilled_colour = bar_unfilled_colour
-            has_any_changed = True
-
-        bar_filled_colour = self.ui_theme.get_colour_or_gradient('filled_bar',
-                                                                 self.combined_element_ids)
-        if bar_filled_colour != self.bar_filled_colour:
-            self.bar_filled_colour = bar_filled_colour
-            has_any_changed = True
-
-        if has_any_changed:
-            self.rebuild()
+    def health_percent(self):
+        return self.current_health / self.health_capacity


Test patch:
diff --git a/tests/data/themes/ui_status_bar_bad_values.json b/tests/data/themes/ui_status_bar_bad_values.json
new file mode 100644
index 00000000..e608df53
--- /dev/null
+++ b/tests/data/themes/ui_status_bar_bad_values.json
@@ -0,0 +1,19 @@
+{
+    "status_bar":
+    {
+        "colours":
+         {
+             "normal_border": "black",
+             "filled_bar": "#f4251b,#FF0000AA,90",
+             "unfilled_bar": "slipper"
+         },
+         "misc":
+         {
+             "shape": "rounded_wrecktangle",
+             "shape_corner_radius": "6.0",
+             "hover_height": "5.0",
+             "border_width": "flip",
+             "shadow_width": "flop"
+         }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/themes/ui_status_bar_no_default.json b/tests/data/themes/ui_status_bar_no_default.json
new file mode 100644
index 00000000..ec454c18
--- /dev/null
+++ b/tests/data/themes/ui_status_bar_no_default.json
@@ -0,0 +1,19 @@
+{
+    "status_bar":
+    {
+        "colours":
+         {
+             "normal_border": "#AAAAAA,#FFFFFF,0",
+             "filled_bar": "#f4251b,#FF0000AA,90",
+             "unfilled_bar": "#CCCCCF"
+         },
+         "misc":
+         {
+             "shape": "rounded_rectangle",
+             "shape_corner_radius": "6",
+             "hover_height": "5",
+             "border_width": "0",
+             "shadow_width": "3"
+         }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_elements/test_ui_status_bar.py b/tests/test_elements/test_ui_status_bar.py
new file mode 100644
index 00000000..7a9f0fa6
--- /dev/null
+++ b/tests/test_elements/test_ui_status_bar.py
@@ -0,0 +1,142 @@
+import os
+import pytest
+import pygame
+
+from tests.shared_comparators import compare_surfaces
+
+from pygame_gui.ui_manager import UIManager
+from pygame_gui.elements.ui_status_bar import UIStatusBar
+
+
+class HealthySprite(pygame.sprite.Sprite):
+    def __init__(self, *groups):
+        super().__init__(*groups)
+        self.current_health = 50
+        self.health_capacity = 100
+        self.rect = pygame.Rect(0, 0, 32, 64)
+
+    def health_percent(self):
+        return self.current_health / self.health_capacity
+
+
+class TestUIStatusBar:
+
+    def test_creation(self, _init_pygame, default_ui_manager):
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                 sprite=healthy_sprite,
+                                 manager=default_ui_manager)
+        assert health_bar.image is not None
+
+    def test_update(self, _init_pygame, default_ui_manager):
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                 sprite=healthy_sprite,
+                                 manager=default_ui_manager)
+        healthy_sprite.current_health = 10
+        health_bar.update(0.01)
+        assert health_bar.image is not None
+
+    def test_rebuild_from_theme_data_non_default(self, _init_pygame):
+        manager = UIManager((800, 600), os.path.join("tests", "data", "themes", "ui_status_bar_no_default.json"))
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                 sprite=healthy_sprite,
+                                 manager=manager)
+        assert health_bar.image is not None
+
+    @pytest.mark.filterwarnings("ignore:Invalid value")
+    @pytest.mark.filterwarnings("ignore:Colour hex code")
+    def test_rebuild_from_theme_data_bad_values(self, _init_pygame):
+        manager = UIManager((800, 600), os.path.join("tests", "data", "themes", "ui_status_bar_bad_values.json"))
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                 sprite=healthy_sprite,
+                                 manager=manager)
+        assert health_bar.image is not None
+
+    def test_set_position(self, _init_pygame, default_ui_manager):
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                 sprite=healthy_sprite,
+                                 manager=default_ui_manager)
+
+        health_bar.set_position((150.0, 30.0))
+
+        assert health_bar.rect.topleft == (150, 30)
+
+    def test_set_relative_position(self, _init_pygame, default_ui_manager):
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                 sprite=healthy_sprite,
+                                 manager=default_ui_manager)
+
+        health_bar.set_relative_position((150.0, 30.0))
+
+        assert health_bar.rect.topleft == (150, 30)
+
+    def test_set_dimensions(self, _init_pygame, default_ui_manager):
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                 sprite=healthy_sprite,
+                                 manager=default_ui_manager)
+
+        health_bar.set_dimensions((250.0, 60.0))
+
+        assert health_bar.drawable_shape.containing_rect.size == (250, 60)
+        assert health_bar.rect.size == (250, 60)
+        assert health_bar.relative_rect.size == (250, 60)
+
+    def test_show(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                 sprite=healthy_sprite,
+                                 manager=default_ui_manager,
+                                 visible=0)
+
+        assert health_bar.visible == 0
+        health_bar.show()
+        assert health_bar.visible == 1
+
+    def test_hide(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                 sprite=healthy_sprite,
+                                 manager=default_ui_manager)
+
+        assert health_bar.visible == 1
+        health_bar.hide()
+        assert health_bar.visible == 0
+
+    def test_show_hide_rendering(self, _init_pygame, default_ui_manager, _display_surface_return_none):
+        resolution = (400, 400)
+        empty_surface = pygame.Surface(resolution)
+        empty_surface.fill(pygame.Color(0, 0, 0))
+
+        surface = empty_surface.copy()
+        manager = UIManager(resolution)
+
+        healthy_sprite = HealthySprite()
+        health_bar = UIStatusBar(relative_rect=pygame.Rect(100, 100, 400, 400),
+                                 sprite=healthy_sprite,
+                                 manager=manager,
+                                 visible=0)
+        manager.update(0.01)
+        manager.draw_ui(surface)
+        assert compare_surfaces(empty_surface, surface)
+
+        surface.fill(pygame.Color(0, 0, 0))
+        health_bar.show()
+        manager.update(0.01)
+        manager.draw_ui(surface)
+        assert not compare_surfaces(empty_surface, surface)
+
+        surface.fill(pygame.Color(0, 0, 0))
+        health_bar.hide()
+        manager.update(0.01)
+        manager.draw_ui(surface)
+        assert compare_surfaces(empty_surface, surface)
+
+
+if __name__ == '__main__':
+    pytest.console_main()