Browse Source

Implemented hover states using a system of bit flags.

David Piuva 2 years ago
parent
commit
e5168474ad

+ 101 - 72
Source/DFPSR/gui/VisualComponent.cpp

@@ -73,6 +73,16 @@ Persistent* VisualComponent::findAttribute(const ReadableString &name) {
 	}
 }
 
+// Pre-condition: component != nullptr
+// Post-condition: Returns the root of component
+static VisualComponent *getRoot(VisualComponent *component) {
+	assert(component != nullptr);
+	while (component->parent != nullptr) {
+		component = component->parent;
+	}
+	return component;
+}
+
 IVector2D VisualComponent::getDesiredDimensions() {
 	// Unless this virtual method is overridden, toolbars and such will try to give these dimensions to the component.
 	return IVector2D(32, 32);
@@ -162,7 +172,7 @@ void VisualComponent::updateChildLocations() {
 static void drawOverlays(ImageRgbaU8& targetImage, VisualComponent &component, const IVector2D& offset) {
 	// Invisible components are not allowed to display overlays, because the component system is
 	//   responsible for visibility settings that specific components are likely to forget about.
-	if (component.getVisible()) {
+	if (component.getVisible() && component.ownsOverlay()) {
 		// Check if the component has the overlay shown.
 		if (component.showingOverlay()) {
 			// Draw the component's own overlay below child overlays. 
@@ -260,10 +270,6 @@ void VisualComponent::detachFromParent() {
 	VisualComponent *parent = this->parent;
 	if (parent != nullptr) {
 		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->defocusChildren();
-		}
 		// 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];
@@ -275,6 +281,8 @@ void VisualComponent::detachFromParent() {
 				return;
 			}
 		}
+		// Update indirect states.
+		getRoot(this)->updateIndirectStates();
 		// 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.
 	}
@@ -368,73 +376,30 @@ std::shared_ptr<VisualComponent> VisualComponent::getShared() {
 	}
 }
 
-// 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->currentState &= ~componentState_focusTail; // 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;
-}
+void VisualComponent::updateStateEvent(ComponentState oldState, ComponentState newState) {}
 
-// 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 {
-				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();
-				this->currentState |= componentState_focusTail;
-				current = parent;
-			}
-		}
+void VisualComponent::updateIndirectStates() {
+	// Call recursively for child components while checking what they contain.
+	ComponentState childStates = 0;
+	for (int i = this->getChildCount() - 1; i >= 0; i--) {
+		this->children[i]->updateIndirectStates();
+		childStates |= this->children[i]->currentState;
 	}
+	// Direct and indirect inheritance.
+	ComponentState expectedIndirectStates = ((childStates & componentState_direct) << 1) | childStates & componentState_indirect;
+	this->currentState = (this->currentState & componentState_direct) | expectedIndirectStates;
 }
 
-void VisualComponent::stateChanged(ComponentState oldState, ComponentState newState) {}
-
 void VisualComponent::sendNotifications() {
+	// Call recursively for child components while checking what they contain.
+	for (int i = this->getChildCount() - 1; i >= 0; i--) {
+		this->children[i]->sendNotifications();
+	}
 	// Detect differences for all flags at once using bits in the integers.
 	if (this->currentState != this->previousState) {
-		stateChanged(this->previousState, this->currentState);
+		updateStateEvent(this->previousState, this->currentState);
 		this->previousState = this->currentState;
 	}
-	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.
@@ -469,17 +434,72 @@ static IVector2D getTotalOffset(const VisualComponent *child, const VisualCompon
 	return result;
 }
 
+// Remove its pointer to its child and the whole trail of focus.
+void VisualComponent::defocusChildren() {
+	for (int i = 0; i < this->getChildCount(); i++) {
+		this->children[i]->applyStateAndMask(~componentState_focus);
+	}
+}
+
+void VisualComponent::addStateBits(ComponentState directStates, bool unique) {
+	VisualComponent *root = getRoot(this);
+	// Remove all focus in the window if unique.
+	if (unique) root->applyStateAndMask(~directStates);
+	// Apply focus directly to itself and indirectly to parents.
+	this->currentState |= directStates;
+	// Update indirect states, so that parent components know what happens to their child components.
+	root->updateIndirectStates();
+}
+
+void VisualComponent::removeStateBits(ComponentState directStates) {
+	VisualComponent *root = getRoot(this);
+	// Apply focus directly to itself and indirectly to parents.
+	this->currentState &= ~directStates;
+	// Update indirect states, so that parent components know what happens to their child components.
+	root->updateIndirectStates();
+}
+
+// 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() {
+	this->addStateBits(componentState_focus, true);
+}
+
+void VisualComponent::hover() {
+	this->addStateBits(componentState_hoverDirect, true);
+}
+
+void VisualComponent::showOverlay() {
+	this->addStateBits(componentState_showingOverlayDirect, false);
+}
+
+// When multiple components are allowed to have the direct flag set, one needs to clean it up like a tree.
+void VisualComponent::hideOverlay() {
+	this->removeStateBits(componentState_showingOverlayDirect);
+}
+
+void VisualComponent::applyStateAndMask(ComponentState keepMask) {
+	this->currentState &= keepMask;
+	for (int i = 0; i < this->getChildCount(); i++) {
+		this->children[i]->applyStateAndMask(keepMask);
+	}
+}
+
 // 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();
+	if (this->parent == nullptr && !recursive) {
+		// Use a combined bit mask for any state that needs to be reset at this time.
+		this->applyStateAndMask(~(componentState_hover));
+		// Update the layout if needed.
+		this->updateChildLocations();
+	}
 	// 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.
+	// Find the component to interact with.
 	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) {
@@ -515,6 +535,9 @@ void VisualComponent::sendMouseEvent(const MouseEvent& event, bool recursive) {
 		// If there is no child component found, interact directly with the parent.
 		MouseEvent parentEvent = event;
 		parentEvent.position += this->location.upperLeft();
+		// Itself is directly hovered.
+		this->hover();
+		// If the event receiver pass it on to child components, it can just reset the hover flags again.
 		this->receiveMouseEvent(parentEvent);
 	}
 	// Release a component on mouse up.
@@ -528,6 +551,7 @@ void VisualComponent::sendMouseEvent(const MouseEvent& event, bool recursive) {
 	}
 	// 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) {
+		//Should not be needed if everything works. this->updateIndirectStates();
 		this->sendNotifications();
 	}
 }
@@ -545,14 +569,19 @@ void VisualComponent::receiveMouseEvent(const MouseEvent& event) {
 }
 
 void VisualComponent::sendKeyboardEvent(const KeyboardEvent& event) {
-	// Send the signal to a focused component or itself
-	if (this->focusComponent.get() != nullptr) {
-		this->focusComponent->sendKeyboardEvent(event);
-	} else {
-		this->receiveKeyboardEvent(event);
+	for (int i = 0; i < this->getChildCount(); i++) {
+		ComponentState state = this->children[i]->currentState;
+		if (state & componentState_focus) {
+			if (state & componentState_focusDirect) {
+				this->children[i]->receiveKeyboardEvent(event);
+			} else if (state & componentState_focusIndirect) {
+				this->children[i]->sendKeyboardEvent(event);
+			}
+		}
 	}
-	// Send focus events in case that any component changed focus.
+	// Check for any state updates.
 	if (this->parent == nullptr) {
+		//Should not be needed if everything works. this->updateIndirectStates();
 		this->sendNotifications();
 	}
 }

+ 51 - 40
Source/DFPSR/gui/VisualComponent.h

@@ -41,15 +41,23 @@ class VisualComponent;
 
 // Bit flags for component states.
 //  The size of ComponentState may change if running out of bits for new flags.
+//  Each state should have a direct state and an indirect state, so that bitwise operations can be used to scan all states at once.
 using ComponentState = uint32_t;
-static const ComponentState componentState_focusTail = 1 << 0;
-static const ComponentState componentState_hoverTail = 1 << 1;
-static const ComponentState componentState_showingOverlay = 1 << 2;
+static const ComponentState componentState_focusDirect = 1 << 0; // Component being directly focused.
+static const ComponentState componentState_focusIndirect = 1 << 1; // Contains the component being focused.
+static const ComponentState componentState_hoverDirect = 1 << 2; // Component being hovered.
+static const ComponentState componentState_hoverIndirect = 1 << 3; // Contains the component being hovered.
+static const ComponentState componentState_showingOverlayDirect = 1 << 4; // The component will have drawOverlay called, if also visible.
+static const ComponentState componentState_showingOverlayIndirect = 1 << 5; // The component contains a component drawing overlays.
+static const ComponentState componentState_focus = componentState_focusDirect | componentState_focusIndirect; // Direct or indirect focus.
+static const ComponentState componentState_hover = componentState_hoverDirect | componentState_hoverIndirect; // Direct or indirect hover.
+static const ComponentState componentState_showingOverlay = componentState_showingOverlayDirect | componentState_showingOverlayIndirect; // Direct or indirect overlay.
+static const ComponentState componentState_direct   = 0b01010101010101010101010101010101;
+static const ComponentState componentState_indirect = 0b10101010101010101010101010101010;
 
 class VisualComponent : public Persistent {
 PERSISTENT_DECLARATION(VisualComponent)
 public: // Relations
-	// 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.
@@ -62,45 +70,46 @@ public: // Relations
 	// Remember the pressed component for sending mouse move events outside of its region.
 	std::shared_ptr<VisualComponent> dragComponent;
 private: // States
-	// Use methods to set the current state, then have it copied to previousState after calling stateChanged in sendNotifications.
+	// Use methods to set the current state, then have it copied to previousState after calling updateStateEvent in sendNotifications.
 	ComponentState currentState, previousState;
-public: // State updates
-	// Looking for recent state changes and sending notifications through stateChanged for each components that had a state change.
+private: // State updates
+	// TODO: Can a faster update be made using a limited traversal from changed component to root, while reusing the same code for the update?
+	// Called after changing direct states.
+	void updateIndirectStates();
+	// Looking for recent state changes and sending notifications through updateStateEvent for each components that had a state change.
 	//   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 a component's state changed, when it is deemed safe to do so.
-	//   All state changes will be sent at the same time, because state changes are often used to trigger other changes.
-	//   Changes to the state made within the notification will not trigger new notifications, because the old state is saved after the call is finished.
-	virtual void stateChanged(ComponentState oldState, ComponentState newState);
-public: // Focus
-	// 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;
-	// All components from root to the clicked component will have the focus flag set, isFocused only care about the focused component holding no more focused child components.
-	inline bool isFocused() { return (this->currentState & componentState_focusTail) && (this->focusComponent.get() == nullptr); }
-	// ownsFOcus is for nested focus, such as automatically closing menu overlays that no longer have focus within them.
-	inline bool ownsFocus() { return (this->currentState & componentState_focusTail) != 0; }
+	// Remove the zeroes in addMask from ones in the component and all child components.
+	void applyStateAndMask(ComponentState keepMask);
+	// Clears directStates from all other components sharing the root, iff unique is true.
+	// Adds directStates to the component.
+	// Updates indirect states based on direct states.
+	void addStateBits(ComponentState directStates, bool unique);
+	// Removes directStates to the component.
+	// Updates indirect states based on direct states.
+	void removeStateBits(ComponentState directStates);
+public: // Focus is reset when a new component is focused.
+	// It was clicked directly last time.
+	inline bool isFocused() { return (this->currentState & componentState_focusDirect) != 0; }
+	// One of its recursive children was clicked last time.
+	inline bool ownsFocus() { return (this->currentState & (componentState_focusDirect | componentState_focusIndirect)) != 0; }
 	// Remove focus from all of the component's children.
 	void defocusChildren();
 	// Give focus to a trail from root to the component.
 	void makeFocused();
+public: // Hover is reset before assigned again using a bit mask.
+	// The cursor hovered within this component without being occluded.
+	bool isHovered() { return (this->currentState & componentState_hoverDirect) != 0; }
+	// The cursor hovered within its region, but one of its recursive children got the direct hover state.
+	bool ownsHover() { return (this->currentState & (componentState_hoverDirect | componentState_hoverIndirect)) != 0; }
+	// Make the component directly hovered and its parents indirectly hovered.
+	void hover();
 public: // Showing overlay
-	inline bool showingOverlay() { return (this->currentState & componentState_showingOverlay) != 0; }
-	inline void showOverlay() { this->currentState |= componentState_showingOverlay; }
-	inline void hideOverlay() { this->currentState &= ~componentState_showingOverlay; }
-public: // Hover
-	// TODO: Implement hover in the same way as grabbing and focus, by always checking which components are hovered.
-	//       One component will be the visibly hovered component and others will keep track of when the mouse enters and exits.
-	//       Come up with a good naming convention for this.
-	// TODO: How can the state be noted if only the tail has a bit.
-	//       Should the directly hovered and focused components have their own head bits?
-	//       One can have combined bit masks for checking if something is a head or tail, for focus and hover flags.
-	// TODO: Make a permanent debug mode where selected states in a mask are drawn on top of components.
-	// bool isHovered() { return (this->currentState & componentState_hoverTail) && (this->hoverComponent.get() == nullptr); }
-	// bool ownsHover() { return (this->currentState & componentState_hoverTail) != 0; }
+	inline bool showingOverlay() { return (this->currentState & componentState_showingOverlayDirect) != 0; }
+	inline bool ownsOverlay() { return (this->currentState & componentState_showingOverlay) != 0; }
+	void showOverlay();
+	void hideOverlay();
 public:
 	// Saved properties
 	FlexRegion region;
@@ -135,8 +144,14 @@ public:
 	String getName() const;
 	void setIndex(int index);
 	int getIndex() const;
-public:
-	// Callbacks
+public: // Internal events that component classes override to know when something has changed.
+	// Called after the component has been created, moved or resized.
+	virtual void updateLocationEvent(const IRect& oldLocation, const IRect& newLocation);
+	// Called after a component's state changed, when it is relatively safe to do so.
+	//   All state changes will be sent at the same time, because state changes are often used to trigger other changes.
+	//   Changes to the state made within the notification will not trigger new notifications, because the old state is saved after the call is finished.
+	virtual void updateStateEvent(ComponentState oldState, ComponentState newState);
+public: // Callbacks that the application use by assigning lambdas to specific components in the interface.
 	DECLARE_CALLBACK(pressedEvent, emptyCallback);
 	DECLARE_CALLBACK(destroyEvent, emptyCallback);
 	DECLARE_CALLBACK(mouseDownEvent, mouseCallback);
@@ -185,7 +200,6 @@ public:
 	//     draw(i, o) <=> drawClipped(i, o, IRect(0, 0, i.width(), i.height()))
 	void drawClipped(ImageRgbaU8 targetImage, const IVector2D& offset, const IRect& clipRegion);
 
-// TODO: Distinguish from the generic version
 	// Add a child component
 	//   Preconditions:
 	//     The parent's component type is a container.
@@ -197,7 +211,6 @@ public:
 	int getChildCount() const override;
 	std::shared_ptr<Persistent> getChild(int index) const override;
 
-// TODO: Reuse in Persistent
 	// Returns true iff child is a member of the component
 	//   Searches recursively
 	bool hasChild(VisualComponent *child) const;
@@ -225,8 +238,6 @@ public:
 	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.

+ 14 - 9
Source/DFPSR/gui/components/Menu.cpp

@@ -130,7 +130,7 @@ void Menu::generateBackground() {
 void Menu::createOverlay() {
 	if (!this->showingOverlay()) {
 		this->showOverlay();
-		this->makeFocused();
+		this->makeFocused(); // Focus on the current menu path to make others lose focus.
 		IRect memberBound = this->children[0]->location;
 		for (int i = 1; i < this->getChildCount(); i++) {
 			memberBound = IRect::merge(memberBound, this->children[i]->location);
@@ -189,16 +189,15 @@ void Menu::changedAttribute(const ReadableString &name) {
 	}
 }
 
-void Menu::stateChanged(ComponentState oldState, ComponentState newState) {
-	// The states lost what they don't have now but had before.
-	ComponentState lostStates = ~newState & oldState;
-	if (lostStates & componentState_focusTail) {
+void Menu::updateStateEvent(ComponentState oldState, ComponentState newState) {
+	// If no longer having any type of focus, hide the overlay.
+	if ((oldState & componentState_focus) && !(newState & componentState_focus)) {
 		// Hide the menu when losing focus.
 		this->hideOverlay();
 		// State notifications are not triggered from within the same notification, so that one can handle all the updates safely in the desired order.
 		this->listBackgroundImage = OrderedImageRgbaU8();
 	}
-	if (lostStates & componentState_showingOverlay) {		
+	if (!(newState & componentState_showingOverlayDirect)) {		
 		// Clean up the background image to save memory and allow it to be regenerated in another size later.
 		this->listBackgroundImage = OrderedImageRgbaU8();
 	}
@@ -229,7 +228,7 @@ void Menu::updateLocationEvent(const IRect& oldLocation, const IRect& newLocatio
 
 static void closeEntireMenu(VisualComponent* menu) {
 	while (menu->parent != nullptr) {
-		// Hide the menu when closing the menu. Notifications to stateChanged will do the proper cleanup for each component's type.
+		// Hide the menu when closing the menu. Notifications to updateStateEvent will do the proper cleanup for each component's type.
 		menu->hideOverlay();
 		// Move on to the parent component.
 		menu = menu->parent;
@@ -266,8 +265,14 @@ void Menu::receiveMouseEvent(const MouseEvent& event) {
 				} 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;
+						VisualComponent *toolbar = this->parent;
+						if (toolbar->ownsFocus()) {
+							for (int i = 0; i < toolbar->getChildCount(); i++) {
+								if (toolbar->children[i]->showingOverlay()) {
+									toggleExpansion = true;
+									break;
+								}
+							}
 						}
 					}
 				}

+ 1 - 1
Source/DFPSR/gui/components/Menu.h

@@ -71,7 +71,7 @@ public:
 	bool hasArrow();
 	// Helper functions for overlay projecting components.
 	void createOverlay();
-	void stateChanged(ComponentState oldState, ComponentState newState) override;
+	void updateStateEvent(ComponentState oldState, ComponentState newState) override;
 	bool pointIsInsideOfOverlay(const IVector2D& pixelPosition) override;
 };
 

+ 20 - 0
Source/SDK/guiExample/media/interface.lof

@@ -69,6 +69,26 @@
 					Name = "menuChapter"
 					Index = 0
 					Text = "Getting started"
+					Begin : Menu
+						Name = "menuChapterStarted"
+						Index = 0
+						Text = "Why you should want to get started"
+					End
+					Begin : Menu
+						Name = "menuChapterStarted"
+						Index = 1
+						Text = "Why do you want to get started"
+					End
+					Begin : Menu
+						Name = "menuChapterStarted"
+						Index = 2
+						Text = "You do not want to get started"
+						Begin : Menu
+							Name = "menuChapterStarted"
+							Index = 3
+							Text = "It's just a mockup application"
+						End
+					End
 				End
 				Begin : Menu
 					Name = "menuChapter"