Browse Source

Introduced overlays and created a menu component using it.

David Piuva 2 years ago
parent
commit
0af571eeed

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

@@ -31,6 +31,7 @@
 #include "components/Label.h"
 #include "components/Picture.h"
 #include "components/Toolbar.h"
+#include "components/Menu.h"
 // <<<< Include new components here
 
 #include "../math/scalar.h"
@@ -51,6 +52,7 @@ void dsr::gui_initialize() {
 		REGISTER_PERSISTENT_CLASS(Label)
 		REGISTER_PERSISTENT_CLASS(Picture)
 		REGISTER_PERSISTENT_CLASS(Toolbar)
+		REGISTER_PERSISTENT_CLASS(Menu)
 		// <<<< Register new components here
 
 		initialized = true;
@@ -147,8 +149,12 @@ void DsrWindow::sendMouseEvent(const MouseEvent& event) {
 	MouseEvent scaledEvent = event / this->pixelScale;
 	// Send the global event
 	this->callback_windowMouseEvent(scaledEvent);
-	// Send to the main panel and its components
-	this->mainPanel->sendMouseEvent(scaledEvent);
+	if (this->mainPanel->getVisible() && this->mainPanel->pointIsInside(scaledEvent.position)) {
+		// In case of the root panel not covering the entire window, adjust input coordinates to the panel's local system.
+		scaledEvent.position -= this->mainPanel->location.upperLeft();
+		// Send to the main panel and its components
+		this->mainPanel->sendMouseEvent(scaledEvent);
+	}
 }
 
 void DsrWindow::sendKeyboardEvent(const KeyboardEvent& event) {

+ 1 - 1
Source/DFPSR/gui/InputEvent.h

@@ -37,7 +37,7 @@ public:
 
 enum class KeyboardEventType { KeyDown, KeyUp, KeyType };
 
-// The DsrKey enumeration is convertible to integers allow certain well defined math operations
+// The DsrKey enumeration is convertible to integers and allow certain well defined math operations
 // Safe assumptions:
 //   * DsrKey_0 to DsrKey_9 are guaranteed to be in an increasing serial order (so that "key - DsrKey_0" is the key's number)
 //   * DsrKey_F1 to DsrKey_F12 are guaranteed to be in an increasing serial order (so that "key - (DsrKey_F1 - 1)" is the key's number)

+ 204 - 71
Source/DFPSR/gui/VisualComponent.cpp

@@ -1,6 +1,6 @@
 // zlib open source license
 //
-// Copyright (c) 2018 to 2019 David Forsgren Piuva
+// Copyright (c) 2018 to 2023 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
@@ -124,6 +124,18 @@ void VisualComponent::updateChildLocations() {
 	}
 }
 
+// Overlays are only cropped by the entire canvas, so the offset is the upper left corner of component relative to the upper left corner of the canvas.
+static void drawOverlays(ImageRgbaU8& targetImage, VisualComponent &component, const IVector2D& offset) {
+	if (component.getVisible()) {
+		// Draw the component's own overlays at the bottom. 
+		component.drawOverlay(targetImage, offset - component.location.upperLeft());
+		// Draw overlays in each child component on top.
+		for (int i = 0; i < component.getChildCount(); i++) {
+			drawOverlays(targetImage, *(component.children[i]), offset + component.children[i]->location.upperLeft());
+		}
+	}
+}
+
 // Offset may become non-zero when the origin is outside of targetImage from being clipped outside of the parent region
 void VisualComponent::draw(ImageRgbaU8& targetImage, const IVector2D& offset) {
 	if (this->getVisible()) {
@@ -131,12 +143,14 @@ void VisualComponent::draw(ImageRgbaU8& targetImage, const IVector2D& offset) {
 		IRect containerBound = this->getLocation() + offset;
 		this->drawSelf(targetImage, containerBound);
 		// Draw each child component
-		for (int i = 0; i < this->getChildCount(); i++) {
-			this->children[i]->drawClipped(targetImage, containerBound.upperLeft(), containerBound);
+		if (!this->managesChildren()) {
+			for (int i = 0; i < this->getChildCount(); i++) {
+				this->children[i]->drawClipped(targetImage, containerBound.upperLeft(), containerBound);
+			}
 		}
-		// Draw the overlays
-		if (this->overlayComponent.get() != nullptr) {
-			this->overlayComponent->drawOverlay(targetImage);
+		// When drawing the root, start recursive drawing of all overlays.
+		if (this->parent == nullptr) {
+			drawOverlays(targetImage, *this, this->location.upperLeft());
 		}
 	}
 }
@@ -156,7 +170,7 @@ void VisualComponent::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLo
 	draw_rectangle(targetImage, relativeLocation, ColorRgbaI32(200, 50, 50, 255));
 }
 
-void VisualComponent::drawOverlay(ImageRgbaU8& targetImage) {}
+void VisualComponent::drawOverlay(ImageRgbaU8& targetImage, const IVector2D &absoluteOffset) {}
 
 // Manual use with the correct type
 void VisualComponent::addChildComponent(std::shared_ptr<VisualComponent> child) {
@@ -209,17 +223,21 @@ void VisualComponent::detachFromParent() {
 		parent->childChanged = true;
 		// If the removed component is focused from the parent, then remove focus so that the parent is focused instead.
 		if (parent->focusComponent.get() == this) {
-			parent->focusComponent = std::shared_ptr<VisualComponent>();
+			parent->defocusChildren();
 		}
-		// Iterate over all children in the parent component
+		// Find the component to detach among the child components.
 		for (int i = 0; i < parent->getChildCount(); i++) {
 			std::shared_ptr<VisualComponent> current = parent->children[i];
 			if (current.get() == this) {
-				current->parent = nullptr; // Assign null
+				// Disconnect parent from child.
+				current->parent = nullptr;
+				// Disconnect child from parent.
 				parent->children.remove(i);
 				return;
 			}
 		}
+		// Any ongoing drag action will allow the component to get the mouse up event to finish transactions safely before being deleted by reference counting.
+		//   Otherwise it may break program logic or cause crashes.
 	}
 }
 
@@ -276,13 +294,17 @@ bool VisualComponent::pointIsInside(const IVector2D& pixelPosition) {
 	    && pixelPosition.y > this->location.top() && pixelPosition.y < this->location.bottom();
 }
 
+bool VisualComponent::pointIsInsideOfOverlay(const IVector2D& pixelPosition) {
+	return false;
+}
+
 // Non-recursive top-down search
-std::shared_ptr<VisualComponent> VisualComponent::getDirectChild(const IVector2D& pixelPosition, bool includeInvisible) {
+std::shared_ptr<VisualComponent> VisualComponent::getDirectChild(const IVector2D& pixelPosition) {
 	// Iterate child components in reverse drawing order
 	for (int i = this->getChildCount() - 1; i >= 0; i--) {
 		std::shared_ptr<VisualComponent> currentChild = this->children[i];
 		// Check if the point is inside the child component
-		if ((currentChild->getVisible() || includeInvisible) && currentChild->pointIsInside(pixelPosition)) {
+		if (currentChild->getVisible() && currentChild->pointIsInside(pixelPosition)) {
 			return currentChild;
 		}
 	}
@@ -290,58 +312,179 @@ std::shared_ptr<VisualComponent> VisualComponent::getDirectChild(const IVector2D
 	return std::shared_ptr<VisualComponent>();
 }
 
-// Recursive top-down search
-std::shared_ptr<VisualComponent> VisualComponent::getTopChild(const IVector2D& pixelPosition, bool includeInvisible) {
-	// Iterate child components in reverse drawing order
-	for (int i = this->getChildCount() - 1; i >= 0; i--) {
-		std::shared_ptr<VisualComponent> currentChild = this->children[i];
-		// Check if the point is inside the child component
-		if ((currentChild->getVisible() || includeInvisible) && currentChild->pointIsInside(pixelPosition)) {
-			// Check if a component inside the child component is even higher up
-			std::shared_ptr<VisualComponent> subChild = currentChild->getTopChild(pixelPosition - this->getLocation().upperLeft(), includeInvisible);
-			if (subChild.get() != nullptr) {
-				return subChild;
+// TODO: Store a pointer to the window in each visual component, so that one can get the shared pointer to the root and get access to clipboard functionality.
+std::shared_ptr<VisualComponent> VisualComponent::getShared() {
+	VisualComponent *parent = this->parent;
+	if (parent == nullptr) {
+		// Not working for the root component, because that would require access to the window.
+		return std::shared_ptr<VisualComponent>();
+	} else {
+		for (int c = 0; c < parent->children.length(); c++) {
+			if (parent->children[c].get() == this) {
+				return parent->children[c];
+			}
+		}
+		// Not found in its own parent if the component tree is broken.
+		return std::shared_ptr<VisualComponent>();
+	}
+}
+
+// Remove its pointer to its child and the whole trail of focus.
+void VisualComponent::defocusChildren() {
+	// Using raw pointers because the this pointer is not reference counted.
+	//   Components should not call arbitrary events that might detach components during processing, until a better way to handle this has been implemented.
+	VisualComponent* parent = this;
+	while (true) {
+		// Get the parent's focused direct child.
+		VisualComponent* child = parent->focusComponent.get();
+		if (child == nullptr) {
+			return; // Reached the end.
+		} else {
+			parent->focusComponent = std::shared_ptr<VisualComponent>(); // The parent removes the focus pointer from the child.
+			child->focused = false; // Remember that it is not focused, for quick access.
+			parent = child; // Prepare for the next iteration.
+		}
+	}
+}
+
+// Pre-condition: component != nullptr
+// Post-condition: Returns the root of component
+VisualComponent *getRoot(VisualComponent *component) {
+	assert(component != nullptr);
+	while (component->parent != nullptr) {
+		component = component->parent;
+	}
+	return component;
+}
+
+// Create a chain of pointers from the root to this component
+//   Any focus pointers that are not along the chain will not count but work as a memory for when one of its parents get focus again.
+void VisualComponent::makeFocused() {
+	VisualComponent* current = this;
+	// Remove any focus tail behind the new focus end.
+	current->defocusChildren();
+	while (current != nullptr) {
+		VisualComponent* parent = current->parent;
+		if (parent == nullptr) {
+			return;
+		} else {
+			VisualComponent* oldFocus = parent->focusComponent.get();
+			if (oldFocus == current) {
+				// When reaching a parent that already points back at the component being focused, there is nothing more to do.
+				return;
 			} else {
-				return currentChild;
+				if (oldFocus != nullptr) {
+					// When reaching a parent that deviated to the old focus branch, follow it and defocus the old components.
+					parent->defocusChildren();
+				}
+				parent->focusComponent = current->getShared();
+				current->focused = true;
+				current = parent;
 			}
 		}
 	}
-	// Return nothing if the point missed all child components
-	return std::shared_ptr<VisualComponent>();
 }
 
-void VisualComponent::sendMouseEvent(const MouseEvent& event) {
-	// Update the layout if needed
+void VisualComponent::sendNotifications() {
+	if (this->focused && !this->previouslyFocused) {
+		this->gotFocus();
+	} else if (!this->focused && this->previouslyFocused) {
+		this->lostFocus();
+	}
+	this->previouslyFocused = this->focused;
+	for (int i = this->getChildCount() - 1; i >= 0; i--) {
+		this->children[i]->sendNotifications();
+	}
+}
+
+// Find the topmost overlay by searching backwards with the parent last and returning a pointer to the component.
+// The point is relative to the upper left corner of component.
+static VisualComponent *getTopmostOverlay(VisualComponent *component, const IVector2D &point) {
+	// Go through child components in reverse draw order to stop when reaching the one that is visible.
+	for (int i = component->getChildCount() - 1; i >= 0; i--) {
+		VisualComponent *result = getTopmostOverlay(component->children[i].get(), point - component->children[i]->location.upperLeft());
+		if (result != nullptr) return result;
+	}
+	// Check itself behind child overlays.
+	if (component->showingOverlay && component->pointIsInsideOfOverlay(point + component->location.upperLeft())) {
+		return component;
+	} else {
+		return nullptr;
+	}
+}
+
+// Get the upper left corner of child relative to the upper left corner of parent.
+//   If parent is null or not a parent of child, then child's offset is relative to the window's canvas.
+static IVector2D getTotalOffset(const VisualComponent *child, const VisualComponent *parent = nullptr) {
+	IVector2D result;
+	while ((child != nullptr) && (child != parent)) {
+		result += child->location.upperLeft();
+		child = child->parent;
+	}
+	return result;
+}
+
+// Takes events with points relative to the upper left corner of the called component.
+void VisualComponent::sendMouseEvent(const MouseEvent& event, bool recursive) {
+	// Update the layout if needed.
 	this->updateChildLocations();
-	// Convert to local coordinates recursively
-	MouseEvent localEvent = event - this->getLocation().upperLeft();
-	std::shared_ptr<VisualComponent> childComponent;
-	// Grab a component on mouse down
-	if (event.mouseEventType == MouseEventType::MouseDown) {
-		childComponent = this->dragComponent = this->focusComponent = this->getDirectChild(localEvent.position, false);
-		this->holdCount++;
+	// Get the point of interaction within the component being sent to,
+	//   so that it can be used to find direct child components expressed
+	//   relative to their container's upper left corner.
+	// If a button is pressed down, this method will try to grab a component to begin mouse interaction.
+	//   Grabbing with the dragComponent pointer makes sure that move and up events can be given even if the cursor moves outside of the component.
+	VisualComponent *childComponent = nullptr;
+	// Find the component to interact with, from pressing down or hovering.
+	if (event.mouseEventType == MouseEventType::MouseDown || this->dragComponent.get() == nullptr) {
+		// Check the overlays first when getting mouse events to the root component.
+		if (this->parent == nullptr) {
+			childComponent = getTopmostOverlay(this, event.position);
+		}
+		// Check for direct child components for passing on the event recursively.
+		//   The sendMouseEvent method can be called recursively from a member of an overlay, so we can't know
+		//   which component is at the top without asking the components that manage interaction with their children.
+		if (childComponent == nullptr && !this->managesChildren()) {
+			std::shared_ptr<VisualComponent> nextContainer = this->getDirectChild(event.position);
+			if (nextContainer.get() != nullptr) {
+				childComponent = nextContainer.get();
+			}
+		}
+	} else if (dragComponent.get() != nullptr) {
+		// If we're grabbing a component, keep sending events to it.
+		childComponent = this->dragComponent.get();
 	}
-	if (this->holdCount > 0) {
-		// If we're grabbing a component, keep sending events to it
-		childComponent = this->dragComponent;
-	} else if (this->getVisible() && this->pointIsInside(event.position)) {
-		// If we're not grabbing a component, see if we can send the action to another component
-		childComponent = this->getDirectChild(localEvent.position, false);
+	// Grab any detected component on mouse down events.
+	if (event.mouseEventType == MouseEventType::MouseDown && childComponent != nullptr) {
+		childComponent->makeFocused();
+		this->dragComponent = childComponent->getShared();
+		this->holdCount++;
 	}
-	// Send the signal to a child component or itself
-	if (childComponent.get() != nullptr) {
+	// Send the signal to a child component or itself.
+	if (childComponent != nullptr) {		
+		// Recalculate local offset through one or more levels of ownership.
+		IVector2D offset = getTotalOffset(childComponent, this);
+		MouseEvent localEvent = event;
+		localEvent.position = event.position - offset;
 		childComponent->sendMouseEvent(localEvent);
 	} else {
-		this->receiveMouseEvent(event);
+		// If there is no child component found, interact directly with the parent.
+		MouseEvent parentEvent = event;
+		parentEvent.position += this->location.upperLeft();
+		this->receiveMouseEvent(parentEvent);
 	}
-	// Release a component on mouse up
+	// Release a component on mouse up.
 	if (event.mouseEventType == MouseEventType::MouseUp) {
 		this->holdCount--;
 		if (this->holdCount <= 0) {
-			this->dragComponent = std::shared_ptr<VisualComponent>(); // Abort drag
+			this->dragComponent = std::shared_ptr<VisualComponent>(); // Abort drag.
+			// Reset when we had more up than down events, in case that the root panel was created with a button already pressed.
 			this->holdCount = 0;
 		}
 	}
+	// Once all focusing and defocusing with arbitrary callbacks is over, send the focus notifications to the components that actually changed focus.
+	if (this->parent == nullptr && !recursive) {
+		this->sendNotifications();
+	}
 }
 
 void VisualComponent::receiveMouseEvent(const MouseEvent& event) {
@@ -363,6 +506,10 @@ void VisualComponent::sendKeyboardEvent(const KeyboardEvent& event) {
 	} else {
 		this->receiveKeyboardEvent(event);
 	}
+	// Send focus events in case that any component changed focus.
+	if (this->parent == nullptr) {
+		this->sendNotifications();
+	}
 }
 
 void VisualComponent::receiveKeyboardEvent(const KeyboardEvent& event) {
@@ -395,33 +542,19 @@ String VisualComponent::call(const ReadableString &methodName, const ReadableStr
 }
 
 bool VisualComponent::isFocused() {
-	if (this->parent != nullptr) {
-		// For child component, go back to the root and then follow the focus pointers to find out which component is focused within the whole tree.
-		//   One cannot just check if the parent points back directly, because old pointers may be left from a previous route.
-		VisualComponent *root = this; while (root->parent != nullptr) { root = root->parent; }
-		VisualComponent *leaf = root; while (leaf->focusComponent.get() != nullptr) { leaf = leaf->focusComponent.get(); }
-		return leaf == this; // Focused if the root component points back to this component and not any further.
-	} else {
-		// Root component is focused if it does not redirect its focus to a child component.
-		return this->focusComponent.get() == nullptr; // Focused if no child is focused.
-	}
+	return this->focused && this->focusComponent.get() == nullptr;
 }
 
-bool VisualComponent::containsFocused() {
-	if (this->parent != nullptr) {
-		// For child component, go back to the root and then follow the focus pointers to find out which component is focused within the whole tree.
-		//   One cannot just check if the parent points back directly, because old pointers may be left from a previous route.
-		VisualComponent *root = this; while (root->parent != nullptr) { root = root->parent; }
-		VisualComponent *current = root;
-		while (current->focusComponent.get() != nullptr) {
-			current = current->focusComponent.get();
-			if (current == this) return true; // Focused if the root component points back to this component somewhere along the way.
-		}
-		return false;
-	} else {
-		// Root component always contains the focused component is focused if it does not redirect its focus to a child component.
-		return this->focusComponent.get() == nullptr; // Focused if no child is focused.
-	}
+bool VisualComponent::ownsFocus() {
+	return this->focused;
+}
+
+// Override these in components to handle changes of focus without having to remember the previous focus.
+void VisualComponent::gotFocus() {}
+void VisualComponent::lostFocus() {}
+
+bool VisualComponent::managesChildren() {
+	return false;
 }
 
 MediaResult dsr::component_generateImage(VisualTheme theme, MediaMethod &method, int width, int height, int red, int green, int blue, int pressed, int focused, int hover) {

+ 57 - 29
Source/DFPSR/gui/VisualComponent.h

@@ -1,6 +1,6 @@
 // zlib open source license
 //
-// Copyright (c) 2018 to 2019 David Forsgren Piuva
+// Copyright (c) 2018 to 2023 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
@@ -40,10 +40,12 @@ MediaResult component_generateImage(VisualTheme theme, MediaMethod &method, int
 class VisualComponent : public Persistent {
 PERSISTENT_DECLARATION(VisualComponent)
 public:
+	// TODO: Make as much as possible private using methods for access, so that things won't break when changes are made.
 	// Parent component
 	VisualComponent *parent = nullptr;
 	IRect givenSpace; // Remembering the local region that was reserved inside of the parent component.
 	bool regionAccessed = false; // If someone requested access to the region, remember to update layout in case of new settings.
+	bool showingOverlay = false; // Should be true when the component is currently projecting an overlay, which requires it to be own focus (be along the focusComponent pointer path from root).
 	// Child components
 	List<std::shared_ptr<VisualComponent>> children;
 	// Remember the component used for a drag event.
@@ -51,14 +53,18 @@ public:
 	int holdCount = 0;
 	// Remember the pressed component for sending mouse move events outside of its region.
 	std::shared_ptr<VisualComponent> dragComponent;
-	// Remember the focused component for keyboard input.
+	// Remember the focused component for keyboard input and showing overlays.
+	// Focus is a trail of pointers from the root to the component that was clicked on.
+	// When makeFocused is called, the old trail will be removed until reaching a common parent with the new focus trail.
+	// When a component's focus changes, changedFocus will be called on it with the new value. 
 	std::shared_ptr<VisualComponent> focusComponent;
-	// The next overlay component, which may refer to one of its children as the next overlay to draw itself on top of other components.
-	//   The linked list goes from the root component in the window, across each component with an active overlay.
-	//   Examples:
-	//     Root -> ToolTipText
-	//     Root -> TopMenu -> SubMenu -> SubMenu
-	std::shared_ptr<VisualComponent> overlayComponent;
+private:
+	// Temporary boolean flags
+	//   Should be private, so that it can later be implemented as bit flags without breaking compatibility.
+	// Keeping track on if the component is focused
+	bool previouslyFocused = false;
+	bool focused = false;
+public:
 	// Saved properties
 	FlexRegion region;
 	PersistentString name;
@@ -135,7 +141,12 @@ public:
 	DECLARE_CALLBACK(keyTypeEvent, keyboardCallback);
 	DECLARE_CALLBACK(selectEvent, indexCallback);
 public:
-	std::shared_ptr<VisualComponent> getDirectChild(const IVector2D& pixelPosition, bool includeInvisible);
+	// Returning a shader pointer to the topmost direct visible child that contains pixelPosition.
+	//   The pixelPosition is relative to the called component's upper left corner.
+	std::shared_ptr<VisualComponent> getDirectChild(const IVector2D& pixelPosition);
+	// Returning a shared pointer to itself.
+	//   Currently not working for the root component because of limitations in C++.
+	std::shared_ptr<VisualComponent> getShared();
 public:
 	// Draw the component
 	//   The component is responsible for drawing the component at this->location + offset.
@@ -151,8 +162,10 @@ public:
 	//   The method is responsible for clipping without a warning when bound is outside of targetImage.
 	virtual void drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation);
 	// Draw the component's overlays on top of other components in the window.
-	//   Overlays are drawn using absolute positions in the window, without caring about any parent location.
-	virtual void drawOverlay(ImageRgbaU8& targetImage);
+	//   Overlays are drawn using absolute positions on the canvas.
+	//   The absoluteOffset is the location of the component's upper left corner relative to the whole window's canvas.
+	// Use for anything that needs to be drawn on top of other components without being clipped by any parent components.
+	virtual void drawOverlay(ImageRgbaU8& targetImage, const IVector2D &absoluteOffset);
 	// Draw the component while skipping pixels outside of clipRegion
 	//   Multiple calls with non-overlapping clip regions should be equivalent to one call with the union of all clip regions.
 	//     This means that the draw methods should handle border clipping so that no extra borderlines or rounded edges appear from nowhere.
@@ -202,33 +215,35 @@ public:
 	// Parent components that place child components automatically can ask them what their minimum useful dimensions are in pixels, so that their text will be visible.
 	// The component can still be resized to less than these dimensions, because the outer components can't give more space than what is given by the window.
 	virtual IVector2D getDesiredDimensions();
+	// Return true to turn off automatic drawing of and interaction with child components.
+	virtual bool managesChildren();
 	// Called after the component has been created, moved or resized.
 	virtual void updateLocationEvent(const IRect& oldLocation, const IRect& newLocation);
+	// Get the component's absolute position relative to the window's client region.
+	//IVector2D getAbsolutePosition();
 	// Calling updateLocationEvent without changing the location, to be used when a child component changed its desired dimensions from altering attributes.
 	bool childChanged = false;
 	// Called before rendering or getting mouse input in case that a child component changed desired dimensions.
 	void updateChildLocations();
-	// Returns true iff the pixel with its upper left corner at pixelPosition is inside the component.
-	// A rectangular bound check with location is used by default.
-	// The caller is responsible for checking if the component is visible when needed.
+	// Returns true iff the pixel relative to the parent container's upper left corner is inside of the component.
+	//   By default, it returns true when pixelPosition is within the component's location, because most component are solid.
+	// The caller is responsible for checking if the component is visible (this->visible.value), so this method would return true if the pixelPosition is inside of an invisible component.
 	virtual bool pointIsInside(const IVector2D& pixelPosition);
-	// Get a pointer to the topmost child
-	// Invisible components are ignored by default, but includeInvisible can be enabled to change that.
-	// Returns an empty reference if the pixel position didn't hit anything in
-	// Since the root component might not be heap allocated, it cannot return itself by reference.
-	//   Use pointIsInside if your root component doesn't cover the whole window.
-	std::shared_ptr<VisualComponent> getTopChild(const IVector2D& pixelPosition, bool includeInvisible = false);
+	// Returns true iff the pixelPosition relative to the parent container's upper left corner is inside of the component's overlay.
+	// The caller is responsible for checking if the component is showing an overlay (this->showOverlay).
+	virtual bool pointIsInsideOfOverlay(const IVector2D& pixelPosition);
 	// Send a mouse down event to the component
-	//   pixelPosition is relative to the parent container.
+	//   pixelPosition is relative to the component's own upper left corner. TODO: Does this make sense, or should it use parent coordinates?
 	//   The component is reponsible for bound checking, which can be used to either block the signal or pass to components below.
-	void sendMouseEvent(const MouseEvent& event);
+	// If recursive is true, notifications will be supressed to prevent duplicate events when called from within receiveMouseEvent.
+	void sendMouseEvent(const MouseEvent& event, bool recursive = false);
 	void sendKeyboardEvent(const KeyboardEvent& event);
 	// Defines what the component does when it has received an event that didn't hit any sub components on the way.
-	//   pixelPosition is relative to the parent container.
-	//   This is not a callback event.
+	//   pixelPosition is relative to the parent's (this->parent) upper left corner.
+	//   This is not a callback event, but a way for the component to handle events.
 	virtual void receiveMouseEvent(const MouseEvent& event);
 	virtual void receiveKeyboardEvent(const KeyboardEvent& event);
-	// Notifies when the theme has been changed, so that temporary data depending on the theme can be replaced
+	// Notifies when the theme has been changed, so that temporary data depending on the theme can be replaced.
 	virtual void changedTheme(VisualTheme newTheme);
 	// Override to be notified about individual attribute changes
 	virtual void changedAttribute(const ReadableString &name) {};
@@ -236,13 +251,26 @@ public:
 	virtual void changedLocation(const IRect &oldLocation, const IRect &newLocation) {};
 	// Custom call handler to manipulate components across a generic API
 	virtual String call(const ReadableString &methodName, const ReadableString &arguments);
-	// Returns true iff the component is focused.
-	//   Used for textboxes to know if they should be drawn as active.
-	//   The root component is considered focused if none of its children are focused.
+	// TODO: Rename to something better.
+	// Returns true iff the component is at the end of the focus trail, not referring to any of its children for focus.
 	bool isFocused();
 	// Returns true iff itself, a direct child or an indirect child has focus.
 	//   Used for menus to keep the whole path of sub-menus alive all the way down to the focused component.
-	bool containsFocused();
+	bool ownsFocus();
+	// Remove focus from all of the component's children and call lostFocus() on them.
+	void defocusChildren();
+	// Give focus to a trail from root to the component and call gotFocus() on them.
+	void makeFocused();
+	// Looking for recent state changes and sending notifications.
+	//   Deferring update notifications using this makes sure that events that trigger updates get to finish before the next one starts.
+	//   This reduces the risk of dead-locks, race-conditions, pointer errors...
+	void sendNotifications();
+	// Called after the component's focus went from false to true.
+	//   Currently not safe to make arbitrary event callbacks that might detach components, due to raw pointer traversal.
+	virtual void gotFocus();
+	// Called after the component's focus went from true to false.
+	//   Currently not safe to make arbitrary event callbacks that might detach components, due to raw pointer traversal.
+	virtual void lostFocus();
 };
 
 }

+ 9 - 0
Source/DFPSR/gui/VisualTheme.cpp

@@ -217,6 +217,15 @@ UR"QUOTE(
 	[Toolbar]
 		border = 1
 		method = "HardRectangle"
+	[MenuTop]
+		border = 1
+		method = "HardRectangle"
+	[MenuSub]
+		border = 1
+		method = "HardRectangle"
+	[MenuList]
+		border = 1
+		method = "HardRectangle"
 )QUOTE";
 
 template <typename V>

+ 1 - 1
Source/DFPSR/gui/components/Button.cpp

@@ -108,7 +108,7 @@ bool Button::pointIsInside(const IVector2D& pixelPosition) {
 	// Get the point relative to the component instead of its direct container
 	IVector2D localPoint = pixelPosition - this->location.upperLeft();
 	// Sample opacity at the location
-	return dsr::image_readPixel_border(this->imageUp, localPoint.x, localPoint.y).alpha > 127;
+	return image_readPixel_border(this->imageUp, localPoint.x, localPoint.y).alpha > 127;
 }
 
 void Button::changedTheme(VisualTheme newTheme) {

+ 266 - 0
Source/DFPSR/gui/components/Menu.cpp

@@ -0,0 +1,266 @@
+// zlib open source license
+//
+// Copyright (c) 2018 to 2023 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 "Menu.h"
+
+using namespace dsr;
+
+PERSISTENT_DEFINITION(Menu)
+
+void Menu::declareAttributes(StructureDefinition &target) const {
+	VisualComponent::declareAttributes(target);
+	target.declareAttribute(U"BackColor");
+	target.declareAttribute(U"ForeColor");
+	target.declareAttribute(U"Text");
+	target.declareAttribute(U"Padding");
+	target.declareAttribute(U"Spacing");
+}
+
+Persistent* Menu::findAttribute(const ReadableString &name) {
+	if (string_caseInsensitiveMatch(name, U"Color") || string_caseInsensitiveMatch(name, U"BackColor")) {
+		// The short Color alias refers to the back color in Buttons, because most buttons use black text.
+		return &(this->backColor);
+	} else if (string_caseInsensitiveMatch(name, U"ForeColor")) {
+		return &(this->foreColor);
+	} else if (string_caseInsensitiveMatch(name, U"Text")) {
+		return &(this->text);
+	} else if (string_caseInsensitiveMatch(name, U"Padding")) {
+		return &(this->padding);
+	} else if (string_caseInsensitiveMatch(name, U"Spacing")) {
+		return &(this->spacing);
+	} else {
+		return VisualComponent::findAttribute(name);
+	}
+}
+
+Menu::Menu() {}
+
+bool Menu::isContainer() const {
+	return true;
+}
+
+static OrderedImageRgbaU8 generateHeadImage(Menu &menu, MediaMethod imageGenerator, int pressed, int width, int height, ColorRgbI32 backColor, ColorRgbI32 foreColor, const ReadableString &text, RasterFont font) {
+	// Create a scaled image
+	OrderedImageRgbaU8 result;
+ 	component_generateImage(menu.getTheme(), imageGenerator, width, height, backColor.red, backColor.green, backColor.blue, pressed)(result);
+	if (string_length(text) > 0) {
+		int left = (image_getWidth(result) - font_getLineWidth(font, text)) / 2;
+		int top = (image_getHeight(result) - font_getSize(font)) / 2;
+		if (pressed) {
+			top += 1;
+		}
+		font_printLine(result, font, text, IVector2D(left, top), ColorRgbaI32(foreColor, 255));
+	}
+	return result;
+}
+
+void Menu::generateGraphics() {
+	int headWidth = this->location.width();
+	int headHeight = this->location.height();
+	if (headWidth < 1) { headWidth = 1; }
+	if (headHeight < 1) { headHeight = 1; }
+	if (!this->hasImages) {
+		completeAssets();
+		this->imageUp = generateHeadImage(*this, this->headImageMethod, 0, headWidth, headHeight, this->backColor.value, this->foreColor.value, this->text.value, this->font);
+		this->imageDown = generateHeadImage(*this, this->headImageMethod, 0, headWidth, headHeight, ColorRgbI32(0, 0, 0), ColorRgbI32(255, 255, 255), this->text.value, this->font);
+		this->hasImages = true;
+	}
+}
+
+// Fill the listBackgroundImageMethod with a solid color
+void Menu::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
+	this->generateGraphics();
+	draw_alphaFilter(targetImage, this->showingOverlay ? this->imageDown : this->imageUp, relativeLocation.left(), relativeLocation.top());
+}
+
+void Menu::generateBackground() {
+	if (!image_exists(this->listBackgroundImage)) {
+		int listWidth = this->overlayLocation.width();
+		int listHeight = this->overlayLocation.height();
+		if (listWidth < 1) { listWidth = 1; }
+		if (listHeight < 1) { listHeight = 1; }
+		component_generateImage(this->theme, this->listBackgroundImageMethod, listWidth, listHeight, this->backColor.value.red, this->backColor.value.green, this->backColor.value.blue)(this->listBackgroundImage);
+	}
+}
+
+void Menu::showOverlay() {
+	// Both showingOverlay and makeFocused are currently required for the overlay to be visible.
+	this->showingOverlay = true;
+	this->makeFocused();
+	IRect memberBound = this->children[0]->location;
+	for (int i = 1; i < this->getChildCount(); i++) {
+		memberBound = IRect::merge(memberBound, this->children[i]->location);
+	}
+	this->overlayLocation = memberBound.expanded(this->padding.value) + this->location.upperLeft();
+}
+
+bool Menu::managesChildren() {
+	return true;
+}
+
+bool Menu::pointIsInsideOfOverlay(const IVector2D& pixelPosition) {
+	return pixelPosition.x > this->overlayLocation.left() && pixelPosition.x < this->overlayLocation.right() && pixelPosition.y > this->overlayLocation.top() && pixelPosition.y < this->overlayLocation.bottom();
+}
+
+// TODO: Draw decorations using headImage.
+void Menu::drawOverlay(ImageRgbaU8& targetImage, const IVector2D &absoluteOffset) {
+	if (this->showingOverlay) {
+		this->generateBackground();
+		// TODO: Let the theme select between solid and alpha filtered drawing.
+		IVector2D overlayOffset = absoluteOffset + this->overlayLocation.upperLeft();
+		draw_copy(targetImage, this->listBackgroundImage, overlayOffset.x, overlayOffset.y);
+		for (int i = 0; i < this->getChildCount(); i++) {
+			this->children[i]->draw(targetImage, absoluteOffset + this->location.upperLeft());
+		}
+	}
+}
+
+void Menu::changedTheme(VisualTheme newTheme) {
+	this->headImageMethod = theme_getScalableImage(newTheme, this->subMenu ? U"MenuSub" : U"MenuTop");
+	this->listBackgroundImageMethod = theme_getScalableImage(newTheme, U"MenuList");
+	this->hasImages = false;
+}
+
+void Menu::completeAssets() {
+	if (this->headImageMethod.methodIndex == -1) {
+		// Work as a sub-menu if the direct parent is also a menu.
+		this->subMenu = this->parent != nullptr && dynamic_cast<Menu*>(this->parent) != nullptr;
+		this->headImageMethod = theme_getScalableImage(theme_getDefault(), this->subMenu ? U"MenuSub" : U"MenuTop");
+		this->listBackgroundImageMethod = theme_getScalableImage(theme_getDefault(), U"MenuList");
+	}
+	if (this->font.get() == nullptr) {
+		this->font = font_getDefault();
+	}
+}
+
+void Menu::changedLocation(const IRect &oldLocation, const IRect &newLocation) {
+	// If the component has changed dimensions then redraw the image
+	if (oldLocation.size() != newLocation.size()) {
+		this->hasImages = false;
+	}
+}
+
+void Menu::changedAttribute(const ReadableString &name) {
+	if (!string_caseInsensitiveMatch(name, U"Visible")) {
+		this->hasImages = false;
+	}
+}
+
+void Menu::lostFocus() {
+	// Hide the menu when losing focus.
+	this->showingOverlay = false;
+	// Erase the list image to save memory.
+	this->listBackgroundImage = OrderedImageRgbaU8();
+}
+
+void Menu::updateLocationEvent(const IRect& oldLocation, const IRect& newLocation) {	
+	int left = this->padding.value;
+	int top = this->padding.value;
+	int overlap = 3;
+	if (this->subMenu) {
+		left += newLocation.width() - overlap;
+	} else {
+		top += newLocation.height() - overlap;
+	}
+	int maxWidth = newLocation.width() - (this->padding.value * 2);
+	for (int i = 0; i < this->getChildCount(); i++) {
+		int width = this->children[i]->getDesiredDimensions().x;
+		if (maxWidth < width) maxWidth = width;
+	}
+	for (int i = 0; i < this->getChildCount(); i++) {
+		int height = this->children[i]->getDesiredDimensions().y;
+		this->children[i]->applyLayout(IRect(left, top, maxWidth, height));
+		top += height + this->spacing.value;
+	}
+}
+
+static void closeEntireMenu(VisualComponent* menu) {
+	while (menu->parent != nullptr) {
+		menu->showingOverlay = false;
+		menu = menu->parent;
+	}
+}
+
+void Menu::receiveMouseEvent(const MouseEvent& event) {
+	int childCount = this->getChildCount();
+	IVector2D positionFromParent = event.position;
+	MouseEvent localEvent = event;
+	localEvent.position -= this->location.upperLeft();
+	if (this->showingOverlay && this->pointIsInsideOfOverlay(event.position)) {
+		for (int i = childCount - 1; i >= 0; i--) {
+			if (this->children[i]->pointIsInside(localEvent.position)) {
+				this->children[i]->makeFocused();
+				MouseEvent childEvent = localEvent;
+				childEvent.position -= this->children[i]->location.upperLeft();
+				this->children[i]->sendMouseEvent(childEvent, true);
+				break;
+			}
+		}
+	} else if (this->pointIsInside(event.position)) {
+		if (childCount > 0) { // Has a list of members to open, toggle expansion when clicked.
+			if (this->subMenu) { // Menu within another menu.
+				// Hover to expand sub-menu's list.
+				if (event.mouseEventType == MouseEventType::MouseMove && !this->showingOverlay) {
+					this->showOverlay();
+				}
+			} else { // Top menu, which is usually placed in a toolbar.
+				bool toggleExpansion = false;
+				if (event.mouseEventType == MouseEventType::MouseDown) {
+					// Toggle expansion when headImageMethod is clicked.
+					toggleExpansion = true;
+				} else if (event.mouseEventType == MouseEventType::MouseMove && !this->showingOverlay) {
+					// Automatically expand hovered top-menus neighboring an opened top menu.
+					if (this->parent != nullptr) {
+						if (this->parent->focusComponent.get() != nullptr && this->parent->focusComponent->showingOverlay) {
+							toggleExpansion = true;
+						}
+					}
+				}
+				if (toggleExpansion) {
+					// Menu components with child members will toggle visibility for its list when pressed.
+					if (this->showingOverlay) {
+						closeEntireMenu(this);
+					} else {
+						this->showOverlay();
+					}
+				}
+			}
+		} else { // List item, because it has no children.
+			// Childless menu components are treated as menu items that can be clicked to perform an action and close the menu.
+			if (event.mouseEventType == MouseEventType::MouseDown) {
+				// Hide overlays all the way to root.
+				closeEntireMenu(this);
+				// Call the event assigned to this menu item.
+				this->callback_pressedEvent();
+			}
+		}
+		// Because the main body was interacted with, the mouse events are passed on.
+		VisualComponent::receiveMouseEvent(event);
+	}
+}
+
+IVector2D Menu::getDesiredDimensions() {
+	this->completeAssets();
+	int sizeAdder = this->padding.value * 2;
+	return IVector2D(font_getLineWidth(this->font, this->text.value) + sizeAdder, font_getSize(this->font) + sizeAdder);
+}

+ 79 - 0
Source/DFPSR/gui/components/Menu.h

@@ -0,0 +1,79 @@
+// zlib open source license
+//
+// Copyright (c) 2018 to 2023 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_MENU
+#define DFPSR_GUI_COMPONENT_MENU
+
+#include "../VisualComponent.h"
+#include "../../api/fontAPI.h"
+
+namespace dsr {
+
+class Menu : public VisualComponent {
+PERSISTENT_DECLARATION(Menu)
+public:
+	// Attributes
+	PersistentColor backColor;
+	PersistentColor foreColor;
+	PersistentString text;
+	PersistentInteger padding = PersistentInteger(4); // Empty space around child components and its own text.
+	PersistentInteger spacing = PersistentInteger(2); // Empty space between child components.
+	void declareAttributes(StructureDefinition &target) const override;
+	Persistent* findAttribute(const ReadableString &name) override;
+private:
+	void completeAssets();
+	void generateGraphics();
+	void generateBackground();
+	MediaMethod headImageMethod, listBackgroundImageMethod;
+	OrderedImageRgbaU8 headImage, listBackgroundImage;
+	RasterFont font;
+	bool subMenu = false;
+	IRect overlayLocation; // Relative to the parent's location, just like its own location
+	// Generated
+	bool hasImages = false;
+	OrderedImageRgbaU8 imageUp;
+	OrderedImageRgbaU8 imageDown;
+public:
+	Menu();
+public:
+	bool isContainer() const override;
+	void drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) override;
+	void drawOverlay(ImageRgbaU8& targetImage, const IVector2D &absoluteOffset) override;
+	void changedTheme(VisualTheme newTheme) override;
+	void changedLocation(const IRect &oldLocation, const IRect &newLocation) override;
+	void changedAttribute(const ReadableString &name) override;
+	void updateLocationEvent(const IRect& oldLocation, const IRect& newLocation) override;
+	void receiveMouseEvent(const MouseEvent& event) override;
+	IVector2D getDesiredDimensions() override;
+	bool managesChildren() override;
+public:
+	// Helper functions for overlay projecting components.
+	void showOverlay();
+	void lostFocus() override;
+	bool pointIsInsideOfOverlay(const IVector2D& pixelPosition) override;
+};
+
+}
+
+#endif
+