In this article we add mouse button input. This is surprisingly complicated, because we have to deal with things like mouse dragging, detecting clicks, and working out what to do if multiple buttons are pressed together, and released in a possibly different order. Luckily, most of the work will be neatly contained in _WindowInputEvent and a single helper function, _WindowSetPressed.

We start by adding the following messages to the Message enumeration:

MSG_LEFT_DOWN,   // Left mouse button pressed. (Sent to the element the mouse cursor is over.)
MSG_LEFT_UP,     // Left mouse button released. (Sent to the element MSG_LEFT_DOWN was sent to.)
MSG_MIDDLE_DOWN, // Middle mouse button pressed. (Sent to the element the mouse cursor is over.)
MSG_MIDDLE_UP,   // Middle mouse button released. (Sent to the element MSG_MIDDLE_DOWN was sent to.)
MSG_RIGHT_DOWN,  // Right mouse button pressed. (Sent to the element the mouse cursor is over.)
MSG_RIGHT_UP,    // Right mouse button released. (Sent to the element MSG_RIGHT_DOWN was sent to.)
MSG_MOUSE_DRAG,  // Mouse moved while holding buttons. (Sent to the element MSG_*_DOWN was sent to.)
MSG_CLICKED,     // Left mouse button released while hovering over the element that MSG_LEFT_UP was sent to.

We also add a Element *pressed; field to Window and define #define UPDATE_PRESSED (2). When an element starts and stops being pressed, we'll send it MSG_UPDATE messages with UPDATE_PRESSED in di. We'll also add a int pressedButton; field to Window, which keeps track of the index of the last pressed mouse button (1 = left, 2 = middle, 3 = right).

Note that mouse button up and drag messages are sent to the same message that received the button down message, not the element the mouse is hovering over. Note that we will ignore mouse down events if another mouse button is already down, and the corresponding up events will be similarly ignored.

Let's write _WindowSetPressed.

void _WindowSetPressed(Window *window, Element *element /* NULL if the mouse button is not being pressed */, int button /* the button that went up or down */) {
	Element *previous = window->pressed;

	// Set the pressed and pressedButtons fields to the new values.
	window->pressed = element;
	window->pressedButton = button;

	// Send out MSG_UPDATE messages.
	if (previous) ElementMessage(previous, MSG_UPDATE, UPDATE_PRESSED, 0);
	if (element) ElementMessage(element, MSG_UPDATE, UPDATE_PRESSED, 0);
}

And let's now use this in the _WindowInputEvent function. This is a solid 60 lines of tricky logic, so buckle up.

void _WindowInputEvent(Window *window, Message message, int di, void *dp) {
	// Is a mouse button pressed?
	if (window->pressed) {
		if (message == MSG_MOUSE_MOVE) {
			// Mouse move events become mouse drag messages, sent to the element we pressed the mouse button down over.
			ElementMessage(window->pressed, MSG_MOUSE_DRAG, di, dp);
		} else if (message == MSG_LEFT_UP && window->pressedButton == 1) {
			// If the left mouse button was released, and this was the button that was pressed to begin with...

			if (window->hovered == window->pressed) {
				// If the mouse cursor is over the pressed element, send it the MSG_CLICKED message.
				ElementMessage(window->pressed, MSG_CLICKED, di, dp);
			}

			// Stop pressing the element.
			ElementMessage(window->pressed, MSG_LEFT_UP, di, dp);
			_WindowSetPressed(window, NULL, 1);
		} else if (message == MSG_MIDDLE_UP && window->pressedButton == 2) {
			// If the middle mouse button was released, and this was the button that was pressed to begin with,
			// stop pressing the element.
			ElementMessage(window->pressed, MSG_MIDDLE_UP, di, dp);
			_WindowSetPressed(window, NULL, 2);
		} else if (message == MSG_RIGHT_UP && window->pressedButton == 3) {
			// If the right mouse button was released, and this was the button that was pressed to begin with,
			// stop pressing the element.
			ElementMessage(window->pressed, MSG_RIGHT_UP, di, dp);
			_WindowSetPressed(window, NULL, 3);
		}
	}

	if (window->pressed) {
		// While a mouse button is held, the hovered element is either the pressed element,
		// or the window element (at the root of the hierarchy).
		// Other elements are not allowed to be considered hovered until the button is released.
		// Here, we update the hovered field and send out MSG_UPDATE messages as necessary.

		bool inside = RectangleContains(window->pressed->clip, window->cursorX, window->cursorY);

		if (inside && window->hovered == &window->e) {
			window->hovered = window->pressed;
			ElementMessage(window->pressed, MSG_UPDATE, UPDATE_HOVERED, 0);
		} else if (!inside && window->hovered == window->pressed) {
			window->hovered = &window->e;
			ElementMessage(window->pressed, MSG_UPDATE, UPDATE_HOVERED, 0);
		}
	} else {
		// No element is currently pressed.
		// Find the element we're hovering over.
		Element *hovered = ElementFindByPoint(&window->e, window->cursorX, window->cursorY);

		if (message == MSG_MOUSE_MOVE) {
			// If the mouse was moved, tell the hovered element.
			ElementMessage(hovered, MSG_MOUSE_MOVE, di, dp);
		} else if (message == MSG_LEFT_DOWN) {
			// If the left mouse button is pressed, start pressing the hovered element.
			_WindowSetPressed(window, hovered, 1);
			ElementMessage(hovered, message, di, dp);
		} else if (message == MSG_MIDDLE_DOWN) {
			// If the middle mouse button is pressed, start pressing the hovered element.
			_WindowSetPressed(window, hovered, 2);
			ElementMessage(hovered, message, di, dp);
		} else if (message == MSG_RIGHT_DOWN) {
			// If the right mouse button is pressed, start pressing the hovered element.
			_WindowSetPressed(window, hovered, 3);
			ElementMessage(hovered, message, di, dp);
		}

		// Update the hovered element if necessary, with the same code as in the previous article.
		if (hovered != window->hovered) {
			Element *previous = window->hovered;
			window->hovered = hovered;
			ElementMessage(previous, MSG_UPDATE, UPDATE_HOVERED, 0);
			ElementMessage(window->hovered, MSG_UPDATE, UPDATE_HOVERED, 0);
		}
	}

	// Repaint the marked region of the window.
	_Update();
}

Finally, we can add the code to the platform layers to forward mouse button events to _WindowInputEvent. As usual, I won't discuss this. However I would like to point out a little detail in the handling of when the mouse cursor leaves the bounds of the window. Previously, we set cursorX and cursorY in the Window structure both to -1 to indicate the mouse was outside the window. However, we now only do this if window->pressed is NULL, so that when the mouse is being dragged the cursor's position is accurate, regardless of whether the cursor is actually in the window. Getting that wrong is a pretty easy mistake to make.

The test usage code has two elements. One of them, the pink one, is set to log all of the messages it receives to stderr. Have a play around with the result and try to understand what's happening.

part9.c

Part 8: Input events and mouse movement.

Part 10: Buttons and labels!