diff --git a/CMake/FileList.cmake b/CMake/FileList.cmake index 81c58fe30..4f77e8007 100644 --- a/CMake/FileList.cmake +++ b/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 diff --git a/Include/RmlUi/Controls/Controls.h b/Include/RmlUi/Controls/Controls.h index 1805bebff..cf487e4a4 100644 --- a/Include/RmlUi/Controls/Controls.h +++ b/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" diff --git a/Include/RmlUi/Controls/ElementProgressBar.h b/Include/RmlUi/Controls/ElementProgressBar.h new file mode 100644 index 000000000..0d1dd22ca --- /dev/null +++ b/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 diff --git a/Include/RmlUi/Controls/ElementTabSet.h b/Include/RmlUi/Controls/ElementTabSet.h index 73b3806bb..4e3cc002f 100644 --- a/Include/RmlUi/Controls/ElementTabSet.h +++ b/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 { diff --git a/Include/RmlUi/Core/ID.h b/Include/RmlUi/Core/ID.h index c0ba751db..a0387db1d 100644 --- a/Include/RmlUi/Core/ID.h +++ b/Include/RmlUi/Core/ID.h @@ -143,6 +143,8 @@ enum class PropertyId : uint16_t Decorator, FontEffect, + FillImage, + NumDefinedIds, FirstCustomId = NumDefinedIds }; diff --git a/Samples/assets/invader.rcss b/Samples/assets/invader.rcss index 413a7c55f..3625b73ec 100644 --- a/Samples/assets/invader.rcss +++ b/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 diff --git a/Samples/assets/invader.tga b/Samples/assets/invader.tga index a6131a2b8..3223e72f3 100644 Binary files a/Samples/assets/invader.tga and b/Samples/assets/invader.tga differ diff --git a/Samples/basic/demo/data/demo.rml b/Samples/basic/demo/data/demo.rml index c8ffc81df..28e0e9015 100644 --- a/Samples/basic/demo/data/demo.rml +++ b/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
Submit
-

Form output

-
+ Sandbox diff --git a/Samples/basic/demo/src/main.cpp b/Samples/basic/demo/src/main.cpp index 42662c2c0..c96a57127 100644 --- a/Samples/basic/demo/src/main.cpp +++ b/Samples/basic/demo/src/main.cpp @@ -108,6 +108,9 @@ class DemoWindow : public Rml::Core::EventListener source->SetValue(value); SetSandboxStylesheet(value); } + + gauge = document->GetElementById("gauge"); + progress_horizontal = document->GetElementById("progress_horizontal"); document->Show(); } @@ -118,6 +121,53 @@ class DemoWindow : public Rml::Core::EventListener { 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 @@ class DemoWindow : public Rml::Core::EventListener 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 @@ class DemoWindow : public Rml::Core::EventListener private: Rml::Core::ElementDocument *document = nullptr; Rml::Core::ElementDocument *iframe = nullptr; + Rml::Core::Element *gauge = nullptr, *progress_horizontal = nullptr; Rml::Core::SharedPtr 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 @@ class DemoEventListener : public Rml::Core::EventListener } else if (value == "submit_form") { - if (Element* el_output = element->GetElementById("form_output")) + const auto& p = event.GetParameters(); + Rml::Core::String output = "

"; + for (auto& entry : p) { - const auto& p = event.GetParameters(); - Rml::Core::String output = "

"; - for (auto& entry : p) - { - auto value = Rml::Core::StringUtilities::EncodeRml(entry.second.Get()); - if (entry.first == "message") - value = "
" + value; - output += "" + entry.first + ": " + value + "
"; - } - output += "

"; - - el_output->SetInnerRML(output); + auto value = Rml::Core::StringUtilities::EncodeRml(entry.second.Get()); + if (entry.first == "message") + value = "
" + value; + output += "" + entry.first + ": " + value + "
"; } + output += "

"; + + 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("Demo sample", Rml::Core::Vector2f(150, 80), context); + demo_window = std::make_unique("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()); diff --git a/Source/Controls/Controls.cpp b/Source/Controls/Controls.cpp index 9155e5a1c..232f7a931 100644 --- a/Source/Controls/Controls.cpp +++ b/Source/Controls/Controls.cpp @@ -54,6 +54,8 @@ struct ElementInstancers { Ptr textarea = std::make_unique>(); Ptr selection = std::make_unique>(); Ptr tabset = std::make_unique>(); + + Ptr progressbar = std::make_unique>(); Ptr datagrid = std::make_unique>(); Ptr datagrid_expand = std::make_unique>(); @@ -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()); diff --git a/Source/Controls/ElementProgressBar.cpp b/Source/Controls/ElementProgressBar.cpp new file mode 100644 index 000000000..4e9c61538 --- /dev/null +++ b/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; + 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; + 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::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; +} + +} +} diff --git a/Source/Core/Box.cpp b/Source/Core/Box.cpp index ecbc91778..1fcf8d2f3 100644 --- a/Source/Core/Box.cpp +++ b/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]; diff --git a/Source/Core/ElementImage.cpp b/Source/Core/ElementImage.cpp index abfd51d93..ca611e82c 100644 --- a/Source/Core/ElementImage.cpp +++ b/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; } diff --git a/Source/Core/StringCache.cpp b/Source/Core/StringCache.cpp index 2d86da9bc..546e9775f 100644 --- a/Source/Core/StringCache.cpp +++ b/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"; diff --git a/Source/Core/StringCache.h b/Source/Core/StringCache.h index f8d434537..0ccfdac9f 100644 --- a/Source/Core/StringCache.h +++ b/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; diff --git a/Source/Core/StyleSheetSpecification.cpp b/Source/Core/StyleSheetSpecification.cpp index 85d330ea1..07958844e 100644 --- a/Source/Core/StyleSheetSpecification.cpp +++ b/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); } diff --git a/changelog.md b/changelog.md index c80251db9..439be22ed 100644 --- a/changelog.md +++ b/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 + + + +``` + ### New font effects