Kaynağa Gözat

Implement progress bar element (progressbar) (#69)

Michael R. P. Ragazzon 6 yıl önce
ebeveyn
işleme
634543b38f

+ 2 - 0
CMake/FileList.cmake

@@ -359,6 +359,7 @@ set(Controls_PUB_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Controls/ElementFormControlInput.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Controls/ElementFormControlSelect.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Controls/ElementFormControlTextArea.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Controls/ElementProgressBar.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Controls/ElementTabSet.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Controls/Header.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Controls/SelectOption.h
@@ -380,6 +381,7 @@ set(Controls_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Controls/ElementFormControlInput.cpp
     ${PROJECT_SOURCE_DIR}/Source/Controls/ElementFormControlSelect.cpp
     ${PROJECT_SOURCE_DIR}/Source/Controls/ElementFormControlTextArea.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Controls/ElementProgressBar.cpp
     ${PROJECT_SOURCE_DIR}/Source/Controls/ElementTabSet.cpp
     ${PROJECT_SOURCE_DIR}/Source/Controls/ElementTextSelection.cpp
     ${PROJECT_SOURCE_DIR}/Source/Controls/InputType.cpp

+ 1 - 0
Include/RmlUi/Controls/Controls.h

@@ -40,6 +40,7 @@
 #include "ElementFormControlInput.h"
 #include "ElementFormControlSelect.h"
 #include "ElementFormControlTextArea.h"
+#include "ElementProgressBar.h"
 #include "ElementTabSet.h"
 #include "SelectOption.h"
 

+ 122 - 0
Include/RmlUi/Controls/ElementProgressBar.h

@@ -0,0 +1,122 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUICONTROLSELEMENTPROGRESSBAR_H
+#define RMLUICONTROLSELEMENTPROGRESSBAR_H
+
+#include "Header.h"
+#include "../Core/Element.h"
+#include "../Core/Geometry.h"
+#include "../Core/Texture.h"
+#include "../Core/Spritesheet.h"
+
+namespace Rml {
+namespace Controls {
+
+/**
+	The 'progressbar' element.
+
+	The 'value' attribute should be a number [0, 1] where 1 means completely filled.
+
+	The 'direction' attribute should be one of:
+		top | right (default) | bottom | left | clockwise | counter-clockwise
+
+	The 'start-edge' attribute should be one of:
+		top (default) | right | bottom | left
+	Only applies to 'clockwise' or 'counter-clockwise' directions. Defines which edge the
+	circle should start expanding from.
+
+	The progressbar generates a non-dom 'fill' element beneath it which can be used to style
+	the filled part of the bar. The 'fill' element can use the 'fill-image'-property to set
+	an image which will be clipped according to the progressbar value. This property is the
+	only way to style a 'clockwise' or 'counter-clockwise' progressbar.
+
+ */
+
+class RMLUICONTROLS_API ElementProgressBar : public Core::Element
+{
+public:
+	RMLUI_RTTI_DefineWithParent(ElementProgressBar, Core::Element)
+
+	/// Constructs a new ElementProgressBar. This should not be called directly; use the Factory instead.
+	/// @param[in] tag The tag the element was declared as in RML.
+	ElementProgressBar(const Core::String& tag);
+	virtual ~ElementProgressBar();
+
+	/// Return the value of the progress bar [0, 1]
+	float GetValue() const;
+
+	/// Set the value of the progress bar
+	void SetValue(float value);
+
+protected:
+	void OnRender() override;
+
+	void OnResize() override;
+
+	void OnAttributeChange(const Core::ElementAttributes& changed_attributes) override;
+
+	void OnPropertyChange(const Core::PropertyIdSet& changed_properties) override;
+
+private:
+	enum class Direction { Top, Right, Bottom, Left, Clockwise, CounterClockwise, Count };
+	enum class StartEdge { Top, Right, Bottom, Left, Count };
+
+	static constexpr Direction DefaultDirection = Direction::Right;
+	static constexpr StartEdge DefaultStartEdge = StartEdge::Top;
+
+	void GenerateGeometry();
+	bool LoadTexture();
+
+	Direction direction;
+	StartEdge start_edge;
+
+	float value;
+
+	Core::Element* fill;
+
+	// The size of the fill geometry as if fully filled, and the offset relative to the 'progressbar' element.
+	Core::Vector2f fill_size, fill_offset;
+
+	// The texture this element is rendering from if the 'fill-image' property is set.
+	Core::Texture texture;
+	bool texture_dirty;
+
+	// The rectangle extracted from a sprite, 'rect_set' controls whether it is active.
+	Core::Rectangle rect;
+	bool rect_set;
+
+	// The geometry used to render this element. Only applies if the 'fill-image' property is set.
+	Core::Geometry geometry;
+	bool geometry_dirty;
+};
+
+}
+}
+
+#endif

+ 0 - 1
Include/RmlUi/Controls/ElementTabSet.h

@@ -30,7 +30,6 @@
 #define RMLUICONTROLSELEMENTTABSET_H
 
 #include "../Core/Element.h"
-#include "../Core/EventListener.h"
 #include "Header.h"
 
 namespace Rml {

+ 2 - 0
Include/RmlUi/Core/ID.h

@@ -143,6 +143,8 @@ enum class PropertyId : uint16_t
 	Decorator,
 	FontEffect,
 
+	FillImage,
+
 	NumDefinedIds,
 	FirstCustomId = NumDefinedIds
 };

+ 9 - 0
Samples/assets/invader.rcss

@@ -119,6 +119,15 @@
 	range-inc:           3px 250px 17px 17px;
 	range-inc-hover:    21px 250px 17px 17px;
 	range-inc-active:   39px 250px 17px 17px;
+	
+	progress-l:      103px 267px 13px 34px;
+	progress-c:      116px 267px 54px 34px;
+	progress-r:      170px 267px 13px 34px;
+	progress-fill-l: 110px 302px  6px 34px;
+	progress-fill-c: 140px 302px  6px 34px;
+	progress-fill-r: 170px 302px  6px 34px;
+	gauge:      0px 271px 100px 86px;
+	gauge-fill: 0px 356px 100px 86px;
 }
 
 body

BIN
Samples/assets/invader.tga


+ 57 - 3
Samples/basic/demo/data/demo.rml

@@ -437,12 +437,58 @@ form h2
 	color: #ffd40f;
 	font-size: 1.7em;
 }
+progressbar {
+	margin: 10px 20px;
+	display: inline-block;
+	vertical-align: middle;
+}
+#gauge { 
+	decorator: image( gauge );
+	width: 100px;
+	height: 86px;
+}
+#gauge fill { 
+	fill-image: gauge-fill;
+}
+#progress_horizontal { 
+	decorator: tiled-horizontal( progress-l, progress-c, progress-r );
+	width: 150px;
+	height: 34px;
+}
+#progress_horizontal fill {
+	decorator: tiled-horizontal( progress-fill-l, progress-fill-c, progress-fill-r );
+	margin: 0 7px;
+	padding-left: 14px;
+}
+#progress_label {
+	font-size: 18px;
+	color: #ceb;
+	margin-left: 1em;
+	margin-bottom: 0;
+}
+#gauge_value, #progress_value {
+	font-size: 18px;
+	color: #4ADB2D;
+	text-align: right;
+	width: 50px;
+	font-effect: outline( 2px #555 );
+}
+#gauge_value {
+	margin: 30px 0 0 18px;
+}
+#progress_value { 
+	margin-left: -20px;
+	display: inline-block;
+	vertical-align: -3px;
+}
+
+
 #form_output 
 {
 	border: 1px #666;
 	font-size: 0.9em;
 	background-color: #ddd;
-	min-height: 30px;
+	min-height: 180px;
 	margin-top: 10px;
 	padding: 5px 8px;
 	color: #222;
@@ -755,8 +801,16 @@ form h2
 		<div style="margin-bottom: 15px;">
 			<input type="submit">Submit</input>
 		</div>
-		<h2>Form output</h2>
-		<div id="form_output"></div>
+		<div id="submit_progress" style="display: none;">
+			<p id="progress_label">&nbsp;</p>
+			<progressbar id="gauge" direction="clockwise" start-edge="bottom" value="0.0">
+				<div id="gauge_value">50%</div>
+			</progressbar>
+			<progressbar id="progress_horizontal" direction="right" value="0.0"/>
+			<div id="progress_value">50%</div>
+			<h2>Form output</h2>
+			<div id="form_output"></div>
+		</div>
 	</form>
 </panel>
 <tab>Sandbox</tab>

+ 79 - 16
Samples/basic/demo/src/main.cpp

@@ -108,6 +108,9 @@ public:
 				source->SetValue(value);
 				SetSandboxStylesheet(value);
 			}
+
+			gauge = document->GetElementById("gauge");
+			progress_horizontal = document->GetElementById("progress_horizontal");
 			
 			document->Show();
 		}
@@ -118,6 +121,53 @@ public:
 		{
 			iframe->UpdateDocument();
 		}
+		if (submitting && gauge && progress_horizontal)
+		{
+			using namespace Rml::Core;
+			constexpr float progressbars_time = 2.f;
+			const float progress = Math::Min(float(GetSystemInterface()->GetElapsedTime() - submitting_start_time) / progressbars_time, 2.f);
+
+			float value_gauge = 1.0f;
+			float value_horizontal = 0.0f;
+			if (progress < 1.0f)
+				value_gauge = 0.5f - 0.5f * Math::Cos(Math::RMLUI_PI * progress);
+			else
+				value_horizontal = 0.5f - 0.5f * Math::Cos(Math::RMLUI_PI * (progress - 1.0f));
+
+			progress_horizontal->SetAttribute("value", value_horizontal);
+
+			const float value_begin = 0.09f;
+			const float value_end = 1.f - value_begin;
+			float value_mapped = value_begin + value_gauge * (value_end - value_begin);
+			gauge->SetAttribute("value", value_mapped);
+
+			auto value_gauge_str = CreateString(10, "%d %%", Math::RoundToInteger(value_gauge * 100.f));
+			auto value_horizontal_str = CreateString(10, "%d %%", Math::RoundToInteger(value_horizontal * 100.f));
+
+			if (auto el_value = document->GetElementById("gauge_value"))
+				el_value->SetInnerRML(value_gauge_str);
+			if (auto el_value = document->GetElementById("progress_value"))
+				el_value->SetInnerRML(value_horizontal_str);
+
+			String label = "Placing tubes";
+			size_t num_dots = (size_t(progress * 10.f) % 4);
+			if (progress > 1.0f)
+				label += "... Placed! Assembling message";
+			if (progress < 2.0f)
+				label += String(num_dots, '.');
+			else
+				label += "... Done!";
+
+			if (auto el_label = document->GetElementById("progress_label"))
+				el_label->SetInnerRML(label);
+
+			if (progress >= 2.0f)
+			{
+				submitting = false;
+				if (auto el_output = document->GetElementById("form_output"))
+					el_output->SetInnerRML(submit_message);
+			}
+		}
 	}
 
 	void Shutdown() {
@@ -159,6 +209,17 @@ public:
 		return document;
 	}
 
+	void SubmitForm(Rml::Core::String in_submit_message) 
+	{
+		submitting = true;
+		submitting_start_time = Rml::Core::GetSystemInterface()->GetElapsedTime();
+		submit_message = in_submit_message;
+		if (auto el_output = document->GetElementById("form_output"))
+			el_output->SetInnerRML("");
+		if (auto el_progress = document->GetElementById("submit_progress"))
+			el_progress->SetProperty("display", "block");
+	}
+
 	void SetSandboxStylesheet(const Rml::Core::String& string)
 	{
 		if (iframe && rml_basic_style_sheet)
@@ -184,7 +245,12 @@ public:
 private:
 	Rml::Core::ElementDocument *document = nullptr;
 	Rml::Core::ElementDocument *iframe = nullptr;
+	Rml::Core::Element *gauge = nullptr, *progress_horizontal = nullptr;
 	Rml::Core::SharedPtr<Rml::Core::StyleSheet> rml_basic_style_sheet;
+
+	bool submitting = false;
+	double submitting_start_time = 0;
+	Rml::Core::String submit_message;
 };
 
 
@@ -201,8 +267,8 @@ struct TweeningParameters {
 
 void GameLoop()
 {
-	context->Update();
 	demo_window->Update();
+	context->Update();
 
 	shell_renderer->PrepareRenderBuffer();
 	context->Render();
@@ -324,21 +390,18 @@ public:
 		}
 		else if (value == "submit_form")
 		{
-			if (Element* el_output = element->GetElementById("form_output"))
+			const auto& p = event.GetParameters();
+			Rml::Core::String output = "<p>";
+			for (auto& entry : p)
 			{
-				const auto& p = event.GetParameters();
-				Rml::Core::String output = "<p>";
-				for (auto& entry : p)
-				{
-					auto value = Rml::Core::StringUtilities::EncodeRml(entry.second.Get<Rml::Core::String>());
-					if (entry.first == "message")
-						value = "<br/>" + value;
-					output += "<strong>" + entry.first + "</strong>: " + value + "<br/>";
-				}
-				output += "</p>";
-
-				el_output->SetInnerRML(output);
+				auto value = Rml::Core::StringUtilities::EncodeRml(entry.second.Get<Rml::Core::String>());
+				if (entry.first == "message")
+					value = "<br/>" + value;
+				output += "<strong>" + entry.first + "</strong>: " + value + "<br/>";
 			}
+			output += "</p>";
+
+			demo_window->SubmitForm(output);
 		}
 		else if (value == "set_sandbox_body")
 		{
@@ -395,7 +458,7 @@ int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
 #endif
 
 	const int width = 1600;
-	const int height = 950;
+	const int height = 900;
 
 	ShellRenderInterfaceOpenGL opengl_renderer;
 	shell_renderer = &opengl_renderer;
@@ -438,7 +501,7 @@ int main(int RMLUI_UNUSED_PARAMETER(argc), char** RMLUI_UNUSED_PARAMETER(argv))
 
 	Shell::LoadFonts("assets/");
 
-	demo_window = std::make_unique<DemoWindow>("Demo sample", Rml::Core::Vector2f(150, 80), context);
+	demo_window = std::make_unique<DemoWindow>("Demo sample", Rml::Core::Vector2f(150, 50), context);
 	demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keydown, demo_window.get());
 	demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Keyup, demo_window.get());
 	demo_window->GetDocument()->AddEventListener(Rml::Core::EventId::Animationend, demo_window.get());

+ 4 - 0
Source/Controls/Controls.cpp

@@ -54,6 +54,8 @@ struct ElementInstancers {
 	Ptr textarea = std::make_unique<ElementInstancerGeneric<ElementFormControlTextArea>>();
 	Ptr selection = std::make_unique<ElementInstancerGeneric<ElementTextSelection>>();
 	Ptr tabset  = std::make_unique<ElementInstancerGeneric<ElementTabSet>>();
+
+	Ptr progressbar  = std::make_unique<ElementInstancerGeneric<ElementProgressBar>>();
 	
 	Ptr datagrid = std::make_unique<ElementInstancerGeneric<ElementDataGrid>>();
 	Ptr datagrid_expand = std::make_unique<ElementInstancerGeneric<ElementDataGridExpandButton>>();
@@ -78,6 +80,8 @@ void RegisterElementInstancers()
 	Core::Factory::RegisterElementInstancer("#selection", element_instancers->selection.get());
 	Core::Factory::RegisterElementInstancer("tabset", element_instancers->tabset.get());
 
+	Core::Factory::RegisterElementInstancer("progressbar", element_instancers->progressbar.get());
+
 	Core::Factory::RegisterElementInstancer("datagrid", element_instancers->datagrid.get());
 	Core::Factory::RegisterElementInstancer("datagridexpand", element_instancers->datagrid_expand.get());
 	Core::Factory::RegisterElementInstancer("#rmlctl_datagridcell", element_instancers->datagrid_cell.get());

+ 389 - 0
Source/Controls/ElementProgressBar.cpp

@@ -0,0 +1,389 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "../../Include/RmlUi/Controls/ElementProgressBar.h"
+#include "../../Include/RmlUi/Core/Math.h"
+#include "../../Include/RmlUi/Core/GeometryUtilities.h"
+#include "../../Include/RmlUi/Core/PropertyIdSet.h"
+#include "../../Include/RmlUi/Core/Factory.h"
+#include "../../Include/RmlUi/Core/ElementDocument.h"
+#include "../../Include/RmlUi/Core/StyleSheet.h"
+#include "../../Include/RmlUi/Core/ElementUtilities.h"
+
+namespace Rml {
+namespace Controls {
+
+ElementProgressBar::ElementProgressBar(const Core::String& tag) : Element(tag), direction(DefaultDirection), start_edge(DefaultStartEdge), value(0), fill(nullptr), rect_set(false), geometry(this)
+{
+	geometry_dirty = false;
+	texture_dirty = true;
+
+	// Add the fill element as a non-DOM element.
+	Core::ElementPtr fill_element = Core::Factory::InstanceElement(this, "*", "fill", Core::XMLAttributes());
+	RMLUI_ASSERT(fill_element);
+	fill = AppendChild(std::move(fill_element), false);
+}
+
+ElementProgressBar::~ElementProgressBar()
+{
+}
+
+float ElementProgressBar::GetValue() const
+{
+	return value;
+}
+
+void ElementProgressBar::SetValue(float in_value)
+{
+	SetAttribute("value", in_value);
+}
+
+void ElementProgressBar::OnRender()
+{
+	// Some properties may change geometry without dirtying the layout, eg. opacity.
+	if (geometry_dirty)
+		GenerateGeometry();
+
+	// Render the geometry at the fill element's content region.
+	geometry.Render(fill->GetAbsoluteOffset().Round());
+}
+
+void ElementProgressBar::OnAttributeChange(const Rml::Core::ElementAttributes& changed_attributes)
+{
+	Rml::Core::Element::OnAttributeChange(changed_attributes);
+
+	if (changed_attributes.find("value") != changed_attributes.end())
+	{
+		value = Core::Math::Clamp( GetAttribute< float >("value", 0.0f), 0.0f, 1.0f);
+		geometry_dirty = true;
+	}
+
+	if (changed_attributes.find("direction") != changed_attributes.end())
+	{
+		using DirectionNameList = std::array<Core::String, size_t(Direction::Count)>;
+		static const DirectionNameList names = { "top", "right", "bottom", "left", "clockwise", "counter-clockwise" };
+
+		direction = DefaultDirection;
+
+		Core::String name = Core::StringUtilities::ToLower( GetAttribute< Core::String >("direction", "") );
+		auto it = std::find(names.begin(), names.end(), name);
+
+		size_t index = size_t(it - names.begin());
+		if (index < size_t(Direction::Count))
+			direction = Direction(index);
+
+		geometry_dirty = true;
+	}
+
+	if (changed_attributes.find("start-edge") != changed_attributes.end())
+	{
+		using StartEdgeNameList = std::array<Core::String, size_t(StartEdge::Count)>;
+		static const StartEdgeNameList names = { "top", "right", "bottom", "left" };
+
+		start_edge = DefaultStartEdge;
+
+		Core::String name = Core::StringUtilities::ToLower(GetAttribute< Core::String >("start-edge", ""));
+		auto it = std::find(names.begin(), names.end(), name);
+
+		size_t index = size_t(it - names.begin());
+		if (index < size_t(StartEdge::Count))
+			start_edge = StartEdge(index);
+
+		geometry_dirty = true;
+	}
+}
+
+void ElementProgressBar::OnPropertyChange(const Core::PropertyIdSet& changed_properties)
+{
+    Element::OnPropertyChange(changed_properties);
+
+    if (changed_properties.Contains(Core::PropertyId::ImageColor) ||
+        changed_properties.Contains(Core::PropertyId::Opacity)) {
+		geometry_dirty = true;
+    }
+
+	if (changed_properties.Contains(Core::PropertyId::FillImage)) {
+		texture_dirty = true;
+	}
+}
+
+void ElementProgressBar::OnResize()
+{
+	using Core::Box;
+	using Core::Vector2f;
+
+	const Vector2f element_size = GetBox().GetSize();
+
+	// Build and set the 'fill' element's box. Here we are mainly interested in all the edge sizes set by the user.
+	// The content size of the box is here scaled to fit inside the progress bar. Then, during 'CreateGeometry()',
+	// the 'fill' element's content size is further shrunk according to 'value' along the proper direction.
+	Box fill_box;
+
+	Core::ElementUtilities::BuildBox(fill_box, element_size, fill);
+	
+	const Vector2f margin_top_left(
+		fill_box.GetEdge(Box::MARGIN, Box::LEFT),
+		fill_box.GetEdge(Box::MARGIN, Box::TOP)
+	);
+	const Vector2f edge_size = fill_box.GetSize(Box::MARGIN) - fill_box.GetSize(Box::CONTENT);
+
+	fill_offset = GetBox().GetPosition() + margin_top_left;
+	fill_size = element_size - edge_size;
+
+	fill_box.SetContent(fill_size);
+	fill->SetBox(fill_box);
+
+	geometry_dirty = true;
+}
+
+void ElementProgressBar::GenerateGeometry()
+{
+	using Core::Vector2f;
+
+	Vector2f render_size = fill_size;
+
+	{
+		// Size and offset the fill element depending on the progressbar value.
+		Vector2f offset = fill_offset;
+
+		switch (direction) {
+		case Direction::Top:
+			render_size.y = fill_size.y * value;
+			offset.y = fill_offset.y + fill_size.y - render_size.y;
+			break;
+		case Direction::Right:
+			render_size.x = fill_size.x * value;
+			break;
+		case Direction::Bottom:
+			render_size.y = fill_size.y * value;
+			break;
+		case Direction::Left:
+			render_size.x = fill_size.x * value;
+			offset.x = fill_offset.x + fill_size.x - render_size.x;
+			break;
+		case Direction::Clockwise:
+		case Direction::CounterClockwise:
+			// Circular progress bars cannot use a box to shape the fill element, instead we need to manually create the geometry from the image texture.
+			// Thus, we leave the size and offset untouched as a canvas for the manual geometry.
+			break;
+
+			RMLUI_UNUSED_SWITCH_ENUM(Direction::Count);
+		}
+
+		Core::Box fill_box = fill->GetBox();
+		fill_box.SetContent(render_size);
+		fill->SetBox(fill_box);
+		fill->SetOffset(offset, this);
+	}
+
+	if (texture_dirty)
+		LoadTexture();
+
+	geometry.Release(true);
+	geometry_dirty = false;
+
+	// If we don't have a fill texture, then there is no need to generate manual geometry, and we are done here.
+	// Instead, users can style the fill element eg. by decorators.
+	if (!texture)
+		return;
+
+	// Otherwise, the 'fill-image' property is set, let's generate its geometry.
+	auto& vertices = geometry.GetVertices();
+	auto& indices = geometry.GetIndices();
+
+	Vector2f texcoords[2];
+	if (rect_set)
+	{
+		Vector2f texture_dimensions((float)texture.GetDimensions(GetRenderInterface()).x, (float)texture.GetDimensions(GetRenderInterface()).y);
+		if (texture_dimensions.x == 0)
+			texture_dimensions.x = 1;
+		if (texture_dimensions.y == 0)
+			texture_dimensions.y = 1;
+
+		texcoords[0].x = rect.x / texture_dimensions.x;
+		texcoords[0].y = rect.y / texture_dimensions.y;
+
+		texcoords[1].x = (rect.x + rect.width) / texture_dimensions.x;
+		texcoords[1].y = (rect.y + rect.height) / texture_dimensions.y;
+	}
+	else
+	{
+		texcoords[0] = Vector2f(0, 0);
+		texcoords[1] = Vector2f(1, 1);
+	}
+
+	Core::Colourb quad_colour;
+	{
+		const Core::ComputedValues& computed = GetComputedValues();
+		const float opacity = computed.opacity;
+		quad_colour = computed.image_color;
+		quad_colour.alpha = (Core::byte)(opacity * (float)quad_colour.alpha);
+	}
+
+
+	switch (direction) 
+	{
+		// For the top, right, bottom, left directions the fill element already describes where we should draw the fill,
+		// we only need to generate the final texture coordinates here.
+	case Direction::Top:    texcoords[0].y = texcoords[0].y + (1.0f - value) * (texcoords[1].y - texcoords[0].y); break;
+	case Direction::Right:  texcoords[1].x = texcoords[0].x + value * (texcoords[1].x - texcoords[0].x);          break;
+	case Direction::Bottom: texcoords[1].y = texcoords[0].y + value * (texcoords[1].y - texcoords[0].y);          break;
+	case Direction::Left:   texcoords[0].x = texcoords[0].x + (1.0f - value) * (texcoords[1].x - texcoords[0].x); break;
+
+	case Direction::Clockwise:
+	case Direction::CounterClockwise:
+	{
+		// The circular directions require custom geometry as a box is insufficient.
+		// We divide the "circle" into eight parts, here called octants, such that each part can be represented by a triangle.
+		// 'num_octants' tells us how many of these are completely or partially filled.
+		const int num_octants = Core::Math::Clamp(Core::Math::RoundUpToInteger(8.f * value), 0, 8);
+		const int num_vertices = 2 + num_octants;
+		const int num_triangles = num_octants;
+		const bool cw = (direction == Direction::Clockwise);
+
+		if (num_octants == 0)
+			break;
+
+		vertices.resize(num_vertices);
+		indices.resize(3 * num_triangles);
+
+		RMLUI_ASSERT(int(start_edge) >= int(StartEdge::Top) && int(start_edge) <= int(StartEdge::Left));
+
+		// The octant our "circle" expands from.
+		const int start_octant = 2 * int(start_edge);
+
+		// Positions along the unit square (clockwise, index 0 on top)
+		const float x[8] = {  0,  1, 1, 1, 0, -1, -1, -1 };
+		const float y[8] = { -1, -1, 0, 1, 1,  1,  0, -1 };
+
+		// Set the position of the octant vertices to be rendered.
+		for (int i = 0; i <= num_octants; i++)
+		{
+			int j = (cw ? i : 8 - i);
+			j = ((j + start_octant) % 8);
+			vertices[i].position = Vector2f(x[j], y[j]);
+		}
+
+		// Find the position of the vertex representing the partially filled triangle.
+		if (value < 1.f)
+		{
+			using namespace Core::Math;
+			const float angle_offset = float(start_octant) / 8.f * 2.f * RMLUI_PI;
+			const float angle = angle_offset + (cw ? 1.f : -1.f) * value * 2.f * RMLUI_PI;
+			Vector2f pos(Sin(angle), -Cos(angle));
+			// Project it from the circle towards the surrounding unit square.
+			pos = pos / Max(AbsoluteValue(pos.x), AbsoluteValue(pos.y));
+			vertices[num_octants].position = pos;
+		}
+
+		const int i_center = num_vertices - 1;
+		vertices[i_center].position = Vector2f(0, 0);
+
+		for (int i = 0; i < num_triangles; i++)
+		{
+			indices[i * 3 + 0] = i_center;
+			indices[i * 3 + 2] = i;
+			indices[i * 3 + 1] = i + 1;
+		}
+
+		for (int i = 0; i < num_vertices; i++)
+		{
+			// Transform position from [-1, 1] to [0, 1] and then to [0, size]
+			const Vector2f pos = (Vector2f(1, 1) + vertices[i].position) * 0.5f;
+			vertices[i].position = pos * render_size;
+			vertices[i].tex_coord = texcoords[0] + pos * (texcoords[1] - texcoords[0]);
+			vertices[i].colour = quad_colour;
+		}
+	}
+		break;
+		RMLUI_UNUSED_SWITCH_ENUM(Direction::Count);
+	}
+
+	const bool is_circular = (direction == Direction::Clockwise || direction == Direction::CounterClockwise);
+
+	if(!is_circular)
+	{
+		vertices.resize(4);
+		indices.resize(6);
+		Rml::Core::GeometryUtilities::GenerateQuad(&vertices[0], &indices[0], Vector2f(0), render_size, quad_colour, texcoords[0], texcoords[1]);
+	}
+}
+
+bool ElementProgressBar::LoadTexture()
+{
+	texture_dirty = false;
+	geometry_dirty = true;
+	rect_set = false;
+
+	Core::String name;
+
+	if (auto property = fill->GetLocalProperty(Core::PropertyId::FillImage))
+		name = property->Get<Core::String>();
+
+	Core::ElementDocument* document = GetOwnerDocument();
+
+	bool texture_set = false;
+
+	if(!name.empty() && document)
+	{
+		// Check for a sprite first, this takes precedence.
+		if (auto& style_sheet = document->GetStyleSheet())
+		{
+			if (const Core::Sprite* sprite = style_sheet->GetSprite(name))
+			{
+				rect = sprite->rectangle;
+				rect_set = true;
+				texture = sprite->sprite_sheet->texture;
+				texture_set = true;
+			}
+		}
+
+		// Otherwise, treat it as a path
+		if (!texture_set)
+		{
+			Core::URL source_url;
+			source_url.SetURL(document->GetSourceURL());
+			texture.Set(name, source_url.GetPath());
+			texture_set = true;
+		}
+	}
+
+	if (!texture_set)
+	{
+		texture = {};
+		rect = {};
+	}
+
+	// Set the texture onto our geometry object.
+	geometry.SetTexture(&texture);
+
+	return true;
+}
+
+}
+}

+ 1 - 1
Source/Core/Box.cpp

@@ -108,7 +108,7 @@ float Box::GetEdge(Area area, Edge edge) const
 float Box::GetCumulativeEdge(Area area, Edge edge) const
 {
 	float size = 0;
-	int max_area = Math::Min((int) area, 2);
+	int max_area = Math::Min((int)area, (int)PADDING);
 	for (int i = 0; i <= max_area; i++)
 		size += area_edges[i][edge];
 

+ 2 - 2
Source/Core/ElementImage.cpp

@@ -69,7 +69,7 @@ bool ElementImage::GetIntrinsicDimensions(Vector2f& _dimensions)
 		dimensions.y = (float)texture.GetDimensions(GetRenderInterface()).y;
 
 	// Return the calculated dimensions. If this changes the size of the element, it will result in
-	// a 'resize' event which is caught below and will regenerate the geometry.
+	// a call to 'onresize' below which will regenerate the geometry.
 	_dimensions = dimensions;
 	return true;
 }
@@ -181,7 +181,7 @@ void ElementImage::GenerateGeometry()
 	
 	Vector2f quad_size = GetBox().GetSize(Rml::Core::Box::CONTENT).Round();
 
-	Rml::Core::GeometryUtilities::GenerateQuad(&vertices[0], &indices[0], Vector2f(0, 0), quad_size, quad_colour,  texcoords[0], texcoords[1]);
+	Rml::Core::GeometryUtilities::GenerateQuad(&vertices[0], &indices[0], Vector2f(0, 0), quad_size, quad_colour, texcoords[0], texcoords[1]);
 
 	geometry_dirty = false;
 }

+ 1 - 0
Source/Core/StringCache.cpp

@@ -110,6 +110,7 @@ const String ANIMATION = "animation";
 const String KEYFRAMES = "keyframes";
 const String OPACITY = "opacity";
 const String POINTER_EVENTS = "pointer-events";
+const String FILL_IMAGE = "fill-image";
 const String MOUSEDOWN = "mousedown";
 const String MOUSESCROLL = "mousescroll";
 const String MOUSEOVER = "mouseover";

+ 1 - 0
Source/Core/StringCache.h

@@ -117,6 +117,7 @@ extern const String KEYFRAMES;
 
 extern const String OPACITY;
 extern const String POINTER_EVENTS;
+extern const String FILL_IMAGE;
 
 extern const String MOUSEDOWN;
 extern const String MOUSESCROLL;

+ 3 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -395,6 +395,9 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	RegisterProperty(PropertyId::Decorator, "decorator", "", false, false).AddParser("string");
 	RegisterProperty(PropertyId::FontEffect, "font-effect", "", true, false).AddParser("string");
 
+	// Rare properties (not added to computed values)
+	RegisterProperty(PropertyId::FillImage, FILL_IMAGE, "", false, false).AddParser("string");
+
 	instance->properties.property_map->AssertAllInserted(PropertyId::NumDefinedIds);
 	instance->properties.shorthand_map->AssertAllInserted(ShorthandId::NumDefinedIds);
 }

+ 74 - 0
changelog.md

@@ -4,6 +4,80 @@
 
 ## RmlUi WIP
 
+### Progress bar
+
+A new `progressbar` element is introduced for visually displaying progress or relative values. The element can take the following attributes.
+
+- `value`. Number `[0, 1]`. The fraction of the progress bar that is filled where 1 means completely filled.
+- `direction`. Determines the direction in which the filled part expands. One of:
+   - `top | right (default) | bottom | left | clockwise | counter-clockwise`
+- `start-edge`. Only applies to 'clockwise' or 'counter-clockwise' directions. Defines which edge the
+circle should start expanding from. Possible values:
+   - `top (default) | right | bottom | left`
+
+The element is only available with the `RmlControls` library.
+
+**Styling**
+
+The progressbar generates a non-dom `fill` element beneath it which can be used to style the filled part of the bar. The `fill` element can use normal properties such as `background-color`, `border`, and `decorator` to style it, or use the new `fill-image`-property to set an image which will be clipped according to the progress bar's `value`. 
+
+The `fill-image` property is the only way to style circular progress bars (`clockwise` and `counter-clockwise` directions). The `fill` element is still available but it will always be fixed in size independent of the `value` attribute.
+
+**New RCSS property**
+
+- `fill-image`. String, non-inherited. Must be the name of a sprite or the path to an image.
+
+**Examples**
+
+The following RCSS styles three different progress bars.
+```css
+@spritesheet progress_bars
+{
+	src: my_progress_bars.tga;
+	progress:        103px 267px 80px 34px;
+	progress-fill-l: 110px 302px  6px 34px;
+	progress-fill-c: 140px 302px  6px 34px;
+	progress-fill-r: 170px 302px  6px 34px;
+	gauge:      0px 271px 100px 86px;
+	gauge-fill: 0px 356px 100px 86px;
+}
+.progress_horizontal { 
+	decorator: image( progress );
+	width: 80px;
+	height: 34px;
+}
+.progress_horizontal fill {
+	decorator: tiled-horizontal( progress-fill-l, progress-fill-c, progress-fill-r );
+	margin: 0 7px;
+	/* padding ensures that the decorator has a minimum width when the value is zero */
+	padding-left: 14px;
+}
+.progress_vertical {
+	width: 30px;
+	height: 80px;
+	background-color: #E3E4E1;
+	border: 4px #A90909;
+}
+.progress_vertical fill {
+	border: 3px #4D9137;
+	background-color: #7AE857;
+}
+.gauge { 
+	decorator: image( gauge );
+	width: 100px;
+	height: 86px;
+}
+.gauge fill { 
+	fill-image: gauge-fill;
+}
+```
+Now, they can be used in RML as follows.
+```html
+<progressbar class="progress_horizontal" value="0.75"/>
+<progressbar class="progress_vertical" direction="top" value="0.6"/>
+<progressbar class="gauge" direction="clockwise" start-edge="bottom" value="0.3"/>
+```
+
 
 ### New font effects