Răsfoiți Sursa

Created a primitive ListBox component. (Without scrollbar and reusing the panel's theme)

David Piuva 5 ani în urmă
părinte
comite
b5c025a866

+ 2 - 0
Source/DFPSR/gui/DsrWindow.cpp

@@ -25,6 +25,7 @@
 #include "DsrWindow.h"
 #include "components/Panel.h"
 #include "components/Button.h"
+#include "components/ListBox.h"
 #include "../math/scalar.h"
 #include "../math/IVector.h"
 #include "../api/imageAPI.h"
@@ -38,6 +39,7 @@ static void initializeGui() {
 		// Register built-in components by name
 		REGISTER_PERSISTENT_CLASS(Panel)
 		REGISTER_PERSISTENT_CLASS(Button)
+		REGISTER_PERSISTENT_CLASS(ListBox)
 		initialized = true;
 	}
 }

+ 243 - 0
Source/DFPSR/gui/components/ListBox.cpp

@@ -0,0 +1,243 @@
+// zlib open source license
+//
+// Copyright (c) 2020 David Forsgren Piuva
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+//    1. The origin of this software must not be misrepresented; you must not
+//    claim that you wrote the original software. If you use this software
+//    in a product, an acknowledgment in the product documentation would be
+//    appreciated but is not required.
+//
+//    2. Altered source versions must be plainly marked as such, and must not be
+//    misrepresented as being the original software.
+//
+//    3. This notice may not be removed or altered from any source
+//    distribution.
+
+#include "ListBox.h"
+#include <math.h>
+
+using namespace dsr;
+
+PERSISTENT_DEFINITION(ListBox)
+
+void ListBox::declareAttributes(StructureDefinition &target) const {
+	VisualComponent::declareAttributes(target);
+	target.declareAttribute(U"Color");
+	target.declareAttribute(U"List");
+	target.declareAttribute(U"SelectedIndex");
+}
+
+Persistent* ListBox::findAttribute(const ReadableString &name) {
+	if (string_caseInsensitiveMatch(name, U"Color")) {
+		return &(this->color);
+	} else if (string_caseInsensitiveMatch(name, U"List")) {
+		return &(this->list);
+	} else if (string_caseInsensitiveMatch(name, U"SelectedIndex")) {
+		return &(this->selectedIndex);
+	} else {
+		return VisualComponent::findAttribute(name);
+	}
+}
+
+ListBox::ListBox() {}
+
+bool ListBox::isContainer() const {
+	return true;
+}
+
+static const int textBorderLeft = 4;
+static const int textBorderTop = 4;
+
+void ListBox::generateGraphics() {
+	int width = this->location.width();
+	int height = this->location.height();
+	if (width < 1) { width = 1; }
+	if (height < 1) { height = 1; }
+	if (!this->hasImages) {
+		completeAssets();
+	 	this->listBox(width, height, this->color.value.red, this->color.value.green, this->color.value.blue)(this->image);
+		int verticalStep = this->font->size;
+		int left = textBorderLeft;
+		int top = textBorderTop;
+		for (int64_t i = this->firstVisible; i < this->list.value.length() && top < height; i++) {
+			ColorRgbaI32 textColor;
+			if (i == this->pressedIndex) {
+				textColor = ColorRgbaI32(255, 255, 255, 255);
+			} else if (i == this->selectedIndex.value) {
+				textColor = ColorRgbaI32(255, 255, 255, 255);
+			} else {
+				textColor = ColorRgbaI32(0, 0, 0, 255);
+			}
+			if (i == this->selectedIndex.value) {
+				draw_rectangle(this->image, IRect(left, top, width - (textBorderLeft * 2), verticalStep), ColorRgbaI32(0, 0, 0, 255));
+			}
+			this->font->printLine(this->image, this->list.value[i], IVector2D(left, top), textColor);
+			top += verticalStep;
+		}
+		this->hasImages = true;
+	}
+}
+
+void ListBox::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
+	this->generateGraphics();
+	draw_copy(targetImage, this->image, relativeLocation.left(), relativeLocation.top());
+}
+
+void ListBox::receiveMouseEvent(const MouseEvent& event) {
+	this->inside = this->pointIsInside(event.position);
+	int64_t maxIndex = this->list.value.length() - 1;
+	int64_t hoverIndex = this->firstVisible + ((event.position.y - this->location.top() - textBorderTop) / this->font->size);
+	if (hoverIndex > maxIndex) {
+		hoverIndex = -1;
+	}
+	if (event.mouseEventType == MouseEventType::MouseDown) {
+		this->pressedIndex = hoverIndex;
+		this->hasImages = false; // Force redraw
+	} else if (this->pressedIndex > -1 && event.mouseEventType == MouseEventType::MouseUp) {
+		if (this->inside && hoverIndex == this->pressedIndex) {
+			this->selectedIndex.value = hoverIndex;
+			this->callback_pressedEvent();
+		}
+		this->pressedIndex = -1;
+		this->hasImages = false; // Force redraw
+	} else if (event.mouseEventType == MouseEventType::Scroll) {
+		if (event.key == MouseKeyEnum::ScrollUp) {
+			this->firstVisible--;
+			if (this->firstVisible < 0) {
+				this->firstVisible = 0;
+			}
+		} else if (event.key == MouseKeyEnum::ScrollDown) {
+			this->firstVisible++;
+			if (this->firstVisible > maxIndex) {
+				this->firstVisible = maxIndex;
+			}
+		}
+		this->hasImages = false; // Force redraw
+	}
+	VisualComponent::receiveMouseEvent(event);
+}
+
+void ListBox::changedTheme(VisualTheme newTheme) {
+	this->listBox = theme_getScalableImage(newTheme, U"Panel");
+	this->hasImages = false; // Force redraw
+}
+
+void ListBox::completeAssets() {
+	if (this->listBox.methodIndex == -1) {
+		this->listBox = theme_getScalableImage(theme_getDefault(), U"Panel");
+	}
+	if (this->font.get() == nullptr) {
+		this->font = font_getDefault();
+	}
+}
+
+void ListBox::changedLocation(IRect &oldLocation, IRect &newLocation) {
+	// If the component has changed dimensions then redraw the image
+	if (oldLocation.size() != newLocation.size()) {
+		this->hasImages = false;
+		this->limitScrolling();
+	}
+}
+
+void ListBox::changedAttribute(const ReadableString &name) {
+	// If an attribute has changed then force the image to be redrawn
+	this->hasImages = false;
+	if (string_caseInsensitiveMatch(name, U"List")) {
+		// Reset selection on full list updates
+		this->selectedIndex.value = 0;
+	}
+	this->limitSelection();
+	this->limitScrolling();
+}
+
+int64_t ListBox::getSelectedIndex() {
+	int64_t index = this->selectedIndex.value;
+	if (this->selectedIndex.value < 0 || this->selectedIndex.value >= this->list.value.length()) {
+		index = -1;
+	}
+	return index;
+}
+
+void ListBox::limitSelection() {
+	// Get the maximum index
+	int maxIndex = this->list.value.length() - 1;
+	if (maxIndex < 0) {
+		maxIndex = 0;
+	}
+	// Reset selection on out of bound
+	if (this->selectedIndex.value < 0 || this->selectedIndex.value > maxIndex) {
+		this->selectedIndex.value = 0;
+	}
+}
+
+// Optional limit of scrolling, to be applied when the user don't explicitly scroll away from the selection
+// limitSelection should be called before limitScrolling, because scrolling limits depend on selection
+void ListBox::limitScrolling() {
+	// Try to load the font before estimating how big the view is
+	this->completeAssets();
+	if (this->font.get() == nullptr) {
+		throwError("Cannot get the font size because ListBox failed to get a font!\n");
+	}
+	int visibleRange = this->location.height() / (this->font->size);
+	int maxScroll = this->selectedIndex.value;
+	int minScroll = maxScroll + 1 - visibleRange;
+	if (this->firstVisible < minScroll) {
+		this->firstVisible = minScroll;
+	} else if (this->firstVisible > maxScroll) {
+		this->firstVisible = maxScroll;
+	}
+}
+
+String ListBox::call(const ReadableString &methodName, const ReadableString &arguments) {
+	if (string_caseInsensitiveMatch(methodName, U"ClearAll")) {
+		// Remove all elements from the list
+		this->list.value.clear();
+		this->hasImages = false;
+		this->selectedIndex.value = 0;
+		this->firstVisible = 0;
+		return U"";
+	} else if (string_caseInsensitiveMatch(methodName, U"PushElement")) {
+		// Push a new element to the list
+		// No quote mangling needed for this single argument.
+		this->list.value.push(arguments);
+		this->selectedIndex.value = this->list.value.length() - 1;
+		this->limitScrolling();
+		this->hasImages = false;
+		return U"";
+	} else if (string_caseInsensitiveMatch(methodName, U"RemoveElement")) {
+		// Remove an element who's index is given in the only input argument
+		int64_t index = string_parseInteger(arguments);
+		if (index < 0 || index >= this->list.value.length()) {
+			throwError("Index (", arguments, " = ", index, ") out of bound in RemoveElement!\n");
+		}
+		this->list.value.remove(index);
+		this->limitSelection();
+		this->limitScrolling();
+		this->hasImages = false;
+		return U"";
+	} else if (string_caseInsensitiveMatch(methodName, U"GetLength")) {
+		// Returns the length of the list
+		return string_combine(this->list.value.length());
+	} else if (string_caseInsensitiveMatch(methodName, U"GetSelectedIndex")) {
+		// Returns the selected index or -1 if nothing is selected
+		return string_combine(this->getSelectedIndex());
+	} else if (string_caseInsensitiveMatch(methodName, U"GetSelectedText")) {
+		// Returns the selected element's text or an empty string if nothing is selected
+		int64_t index = getSelectedIndex();
+		if (index > -1) {
+			return this->list.value[index];
+		} else {
+			return U"";
+		}
+	} else {
+		return VisualComponent::call(methodName, arguments);
+	}
+}

+ 75 - 0
Source/DFPSR/gui/components/ListBox.h

@@ -0,0 +1,75 @@
+// zlib open source license
+//
+// Copyright (c) 2020 David Forsgren Piuva
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+//    1. The origin of this software must not be misrepresented; you must not
+//    claim that you wrote the original software. If you use this software
+//    in a product, an acknowledgment in the product documentation would be
+//    appreciated but is not required.
+//
+//    2. Altered source versions must be plainly marked as such, and must not be
+//    misrepresented as being the original software.
+//
+//    3. This notice may not be removed or altered from any source
+//    distribution.
+
+#ifndef DFPSR_GUI_COMPONENT_LISTBOX
+#define DFPSR_GUI_COMPONENT_LISTBOX
+
+#include "../VisualComponent.h"
+#include "../Font.h"
+
+namespace dsr {
+
+class ListBox : public VisualComponent {
+PERSISTENT_DECLARATION(ListBox)
+public:
+	// Attributes
+	PersistentColor color;
+	PersistentStringList list;
+	PersistentInteger selectedIndex; // Should always be inside of the list's 0..length-1 bound or zero.
+	void declareAttributes(StructureDefinition &target) const override;
+	Persistent* findAttribute(const ReadableString &name) override;
+private:
+	// Temporary
+	bool pressed = false;
+	bool inside = false;
+	int64_t pressedIndex = -1; // Index of pressed item or -1 for none.
+	int64_t firstVisible = 0; // Index of first visible element for scrolling. May never go below zero.
+	// Given from the style
+	MediaMethod listBox;
+	std::shared_ptr<RasterFont> font;
+	void completeAssets();
+	void generateGraphics();
+	// Generated
+	bool hasImages = false;
+	OrderedImageRgbaU8 image;
+	// Helper methods
+	// Returns the selected index referring to an existing element or -1 if none is selected
+	int64_t getSelectedIndex();
+	void limitSelection();
+	void limitScrolling();
+public:
+	ListBox();
+public:
+	bool isContainer() const;
+	void drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) override;
+	void receiveMouseEvent(const MouseEvent& event) override;
+	void changedTheme(VisualTheme newTheme) override;
+	void changedLocation(IRect &oldLocation, IRect &newLocation) override;
+	void changedAttribute(const ReadableString &name) override;
+	// The call receiver decides if the input needs to be mangled into quotes
+	String call(const ReadableString &methodName, const ReadableString &arguments) override;
+};
+
+}
+
+#endif