// This file is part of the Essence operating system. // It is released under the terms of the MIT license -- see LICENSE.md. // Written by: nakst. #ifndef IMPLEMENTATION // TODO Don't send key released messages if the focused window has changed. // TODO Blur clamping is incorrect with minimal repainting! // TODO Cursor trails. // Terminology: // Dynamic resize - flicker-free resizing in container windows with an embedded window owned by a separate process. // Direct update - paint first onto the video card's framebuffer, then onto the window manager's; used to reduce latency. struct EmbeddedWindow { void Destroy(); void Close(); void SetEmbedOwner(Process *process); Process *volatile owner; void *volatile apiWindow; volatile uint32_t handles; struct Window *container; EsObjectID id; bool closed; }; struct Window { void Update(EsRectangle *region, bool addToModifiedRegion); bool UpdateDirect(K_USER_BUFFER void *bits, uintptr_t stride, EsRectangle region); void Destroy(); void Close(); bool Move(EsRectangle newBounds, uint32_t flags); void SetEmbed(EmbeddedWindow *window); bool IsVisible(); void ResizeEmbed(); // Send a message to the embedded window telling it to resize. // State: EsWindowStyle style; EsRectangle solidOffsets, embedInsets; bool solid, noClickActivate, hidden, isMaximised, alwaysOnTop, hoveringOverEmbed, activationClick, noBringToFront; volatile bool closed; // Appearance: Surface surface; EsRectangle opaqueBounds, blurBounds; uint8_t alpha, material; // Owner and children: Process *owner; void *apiWindow; EmbeddedWindow *embed; volatile uint32_t handles; EsObjectID id; // Location: EsPoint position; size_t width, height; }; struct WindowManager { void *CreateWindow(Process *process, void *apiWindow, EsWindowStyle style); void *CreateEmbeddedWindow(Process *process, void *apiWindow); Window *FindWindowAtPosition(int cursorX, int cursorY, EsObjectID exclude = 0); void Initialise(); void MoveCursor(int64_t xMovement, int64_t yMovement); void ClickCursor(uint32_t buttons); void ScrollWheel(int32_t dx, int32_t dy); void PressKey(uint32_t scancode); void Redraw(EsPoint position, int width, int height, Window *except = nullptr, int startingAt = 0, bool addToModifiedRegion = true); bool ActivateWindow(Window *window); // Returns true if any menus were closed. void HideWindow(Window *window); Window *FindWindowToActivate(Window *excluding = nullptr); uintptr_t GetActivationZIndex(); void ChangeWindowDepth(Window *window, bool alwaysRedraw, ptrdiff_t newZDepth); intptr_t FindWindowDepth(Window *window); bool CloseMenus(); // Returns true if any menus were closed. void StartEyedrop(uintptr_t object, Window *avoid, uint32_t cancelColor); void EndEyedrop(bool cancelled); bool initialised; // Windows: Array windows; // Sorted by z. Array embeddedWindows; Window *pressedWindow, *activeWindow, *hoverWindow; KMutex mutex; KEvent windowsToCloseEvent; EsObjectID currentWindowID; size_t inspectorWindowCount; EsMessageType pressedWindowButton; // Cursor: int32_t cursorX, cursorY; int32_t cursorXPrecise, cursorYPrecise; // Scaled up by a factor of K_CURSOR_MOVEMENT_SCALE. uint32_t lastButtons; Surface cursorSurface, cursorSwap, cursorTemporary; int cursorImageOffsetX, cursorImageOffsetY; uintptr_t cursorID; bool cursorShadow; bool changedCursorImage; uint32_t cursorProperties; // Keyboard: bool numlock; uint8_t leftModifiers, rightModifiers; uint16_t keysHeld, maximumKeysHeld /* cleared when all released */; uint8_t keysHeldBitSet[512 / 8]; // Eyedropper: uintptr_t eyedropObject; bool eyedropping; Process *eyedropProcess; uint64_t eyedropAvoidID; uint32_t eyedropCancelColor; // Miscellaneous: EsRectangle workArea; // Devices: KMutex deviceMutex; Array hiDevices; EsGameControllerState gameControllers[ES_GAME_CONTROLLER_MAX_COUNT]; size_t gameControllerCount; EsObjectID gameControllerID; // Flicker-free resizing: #define RESIZE_FLICKER_TIMEOUT_MS (40) #define RESIZE_SLOW_THRESHOLD (RESIZE_FLICKER_TIMEOUT_MS * 3 / 4) Window *resizeWindow; bool resizeReceivedBitsFromContainer; bool resizeReceivedBitsFromEmbed; uint64_t resizeStartTimeStampMs; EsRectangle resizeQueuedRectangle; bool resizeQueued; bool resizeSlow; // Set if the previous resize went past RESIZE_FLICKER_SLOW_THRESHOLD; // when set, the old surface bits are copied on resize, so that if the resize times out the result will be reasonable. }; WindowManager windowManager; #else void HIDeviceUpdateIndicators() { KMutexAssertLocked(&windowManager.mutex); KMutexAcquire(&windowManager.deviceMutex); // TODO Other indicators. uint32_t indicators = windowManager.numlock ? K_KEYBOARD_INDICATOR_NUM_LOCK : 0; for (uintptr_t i = 0; i < windowManager.hiDevices.Length(); i++) { KHIDevice *device = (KHIDevice *) windowManager.hiDevices[i]->parent; if (device->setKeyboardIndicators) { device->setKeyboardIndicators(device, indicators); } } KMutexRelease(&windowManager.deviceMutex); } void HIDeviceRemoved(KDevice *device) { KMutexAcquire(&windowManager.deviceMutex); windowManager.hiDevices.FindAndDeleteSwap(device, true); KMutexRelease(&windowManager.deviceMutex); } void KRegisterHIDevice(KHIDevice *device) { KDevice *child = KDeviceCreate("HID child", device, sizeof(KDevice)); if (child) { KMutexAcquire(&windowManager.deviceMutex); child->removed = HIDeviceRemoved; windowManager.hiDevices.Add(child); KMutexRelease(&windowManager.deviceMutex); KDeviceCloseHandle(child); } KDeviceCloseHandle(device); } bool Window::IsVisible() { return !hidden && !closed && (id != windowManager.eyedropAvoidID || !windowManager.eyedropping); } void SendMessageToWindow(Window *window, EsMessage *message) { KMutexAssertLocked(&windowManager.mutex); if (window->closed) { return; } if (!window->owner->handles) { KernelPanic("SendMessageToWindow - (%x:%d/%x:%d) No handles.\n", window, window->handles, window->owner, window->owner->handles); } if (window->style != ES_WINDOW_CONTAINER || !window->embed) { window->owner->messageQueue.SendMessage(window->apiWindow, message); return; } if (message->type == ES_MSG_WINDOW_RESIZED) { message->windowResized.content = ES_RECT_2S(window->width, window->height); window->owner->messageQueue.SendMessage(window->apiWindow, message); message->windowResized.content = ES_RECT_2S(window->width - window->embedInsets.l - window->embedInsets.r, window->height - window->embedInsets.t - window->embedInsets.b); window->embed->owner->messageQueue.SendMessage(window->embed->apiWindow, message); } else if (message->type == ES_MSG_WINDOW_DEACTIVATED || message->type == ES_MSG_WINDOW_ACTIVATED) { window->owner->messageQueue.SendMessage(window->apiWindow, message); window->embed->owner->messageQueue.SendMessage(window->embed->apiWindow, message); } else if (message->type == ES_MSG_MOUSE_MOVED) { EsRectangle embedRegion = ES_RECT_4(window->embedInsets.l, window->width - window->embedInsets.r, window->embedInsets.t, window->height - window->embedInsets.b); bool inEmbed = windowManager.pressedWindow ? window->hoveringOverEmbed : EsRectangleContains(embedRegion, message->mouseMoved.newPositionX, message->mouseMoved.newPositionY); if (inEmbed) { message->mouseMoved.newPositionX -= window->embedInsets.l; message->mouseMoved.newPositionY -= window->embedInsets.t; window->embed->owner->messageQueue.SendMessage(window->embed->apiWindow, message); if (!windowManager.pressedWindow && !window->hoveringOverEmbed) { message->type = ES_MSG_MOUSE_EXIT; window->owner->messageQueue.SendMessage(window->apiWindow, message); } } else { window->owner->messageQueue.SendMessage(window->apiWindow, message); if (!windowManager.pressedWindow && window->hoveringOverEmbed) { message->type = ES_MSG_MOUSE_EXIT; window->embed->owner->messageQueue.SendMessage(window->embed->apiWindow, message); } } if (!windowManager.pressedWindow) { window->hoveringOverEmbed = inEmbed; } } else if (message->type >= ES_MSG_MOUSE_LEFT_DOWN && message->type <= ES_MSG_MOUSE_MIDDLE_UP) { if (window->hoveringOverEmbed) { message->mouseDown.positionX -= window->embedInsets.l; message->mouseDown.positionY -= window->embedInsets.t; if (!window->activationClick) { window->embed->owner->messageQueue.SendMessage(window->embed->apiWindow, message); } } else { window->owner->messageQueue.SendMessage(window->apiWindow, message); } } else if (message->type == ES_MSG_SCROLL_WHEEL) { if (window->hoveringOverEmbed) { window->embed->owner->messageQueue.SendMessage(window->embed->apiWindow, message); } else { window->owner->messageQueue.SendMessage(window->apiWindow, message); } } else if (message->type == ES_MSG_KEY_DOWN || message->type == ES_MSG_KEY_UP) { window->embed->owner->messageQueue.SendMessage(window->embed->apiWindow, message); if ((message->keyboard.modifiers & (ES_MODIFIER_CTRL | ES_MODIFIER_ALT | ES_MODIFIER_FLAG | ES_MODIFIER_ALT_GR)) || message->keyboard.scancode == ES_SCANCODE_LEFT_CTRL || message->keyboard.scancode == ES_SCANCODE_RIGHT_CTRL || message->keyboard.scancode == ES_SCANCODE_LEFT_SHIFT || message->keyboard.scancode == ES_SCANCODE_RIGHT_SHIFT || message->keyboard.scancode == ES_SCANCODE_LEFT_ALT || message->keyboard.scancode == ES_SCANCODE_RIGHT_ALT || message->keyboard.scancode == ES_SCANCODE_LEFT_FLAG || message->keyboard.scancode == ES_SCANCODE_RIGHT_FLAG) { // Additionally send the keyboard input message to the embed parent // if Ctrl, Alt or Flag were held, or this is a modifier change message. // (Otherwise don't, to avoid increasing typing latency.) window->owner->messageQueue.SendMessage(window->apiWindow, message); } } else if (message->type == ES_MSG_MOUSE_EXIT) { window->embed->owner->messageQueue.SendMessage(window->embed->apiWindow, message); window->owner->messageQueue.SendMessage(window->apiWindow, message); } else { window->owner->messageQueue.SendMessage(window->apiWindow, message); } } Window *WindowManager::FindWindowAtPosition(int cursorX, int cursorY, EsObjectID exclude) { KMutexAssertLocked(&mutex); for (intptr_t i = windows.Length() - 1; i >= 0; i--) { Window *window = windows[i]; EsRectangle bounds = ES_RECT_4PD(window->position.x, window->position.y, window->width, window->height); if (window->solid && !window->hidden && exclude != window->id && EsRectangleContains(EsRectangleAdd(bounds, window->solidOffsets), cursorX, cursorY) && (!window->isMaximised || EsRectangleContains(workArea, cursorX, cursorY))) { return window; } } return nullptr; } void WindowManager::EndEyedrop(bool cancelled) { KMutexAssertLocked(&mutex); eyedropping = false; EsMessage m = { ES_MSG_EYEDROP_REPORT }; uint32_t color = *(uint32_t *) ((uint8_t *) graphics.frameBuffer.bits + 4 * cursorX + graphics.frameBuffer.stride * cursorY) | 0xFF000000; m.eyedrop.color = cancelled ? eyedropCancelColor : color; m.eyedrop.cancelled = cancelled; eyedropProcess->messageQueue.SendMessage((void *) eyedropObject, &m); CloseHandleToObject(eyedropProcess, KERNEL_OBJECT_PROCESS); eyedropProcess = nullptr; Redraw(ES_POINT(0, 0), graphics.width, graphics.height); } void WindowManager::PressKey(uint32_t scancode) { if (!initialised) { return; } KMutexAcquire(&mutex); if (scancode == ES_SCANCODE_NUM_DIVIDE && (leftModifiers & ES_MODIFIER_FLAG)) { KernelPanic("WindowManager::PressKey - Panic key pressed.\n"); } if (eyedropping) { if (scancode == (ES_SCANCODE_ESCAPE | K_SCANCODE_KEY_RELEASED)) { EndEyedrop(true); } MoveCursor(0, 0); KMutexRelease(&mutex); return; } // TODO Caps lock. if (scancode == ES_SCANCODE_LEFT_CTRL) leftModifiers |= ES_MODIFIER_CTRL; if (scancode == (ES_SCANCODE_LEFT_CTRL | K_SCANCODE_KEY_RELEASED)) leftModifiers &= ~ES_MODIFIER_CTRL; if (scancode == ES_SCANCODE_LEFT_SHIFT) leftModifiers |= ES_MODIFIER_SHIFT; if (scancode == (ES_SCANCODE_LEFT_SHIFT | K_SCANCODE_KEY_RELEASED)) leftModifiers &= ~ES_MODIFIER_SHIFT; if (scancode == ES_SCANCODE_LEFT_ALT) leftModifiers |= ES_MODIFIER_ALT; if (scancode == (ES_SCANCODE_LEFT_ALT | K_SCANCODE_KEY_RELEASED)) leftModifiers &= ~ES_MODIFIER_ALT; if (scancode == ES_SCANCODE_LEFT_FLAG) leftModifiers |= ES_MODIFIER_FLAG; if (scancode == (ES_SCANCODE_LEFT_FLAG | K_SCANCODE_KEY_RELEASED)) leftModifiers &= ~ES_MODIFIER_FLAG; if (scancode == ES_SCANCODE_RIGHT_CTRL) leftModifiers |= ES_MODIFIER_CTRL; if (scancode == (ES_SCANCODE_RIGHT_CTRL | K_SCANCODE_KEY_RELEASED)) leftModifiers &= ~ES_MODIFIER_CTRL; if (scancode == ES_SCANCODE_RIGHT_SHIFT) leftModifiers |= ES_MODIFIER_SHIFT; if (scancode == (ES_SCANCODE_RIGHT_SHIFT | K_SCANCODE_KEY_RELEASED)) leftModifiers &= ~ES_MODIFIER_SHIFT; if (scancode == ES_SCANCODE_RIGHT_ALT) leftModifiers |= ES_MODIFIER_ALT_GR; if (scancode == (ES_SCANCODE_RIGHT_ALT | K_SCANCODE_KEY_RELEASED)) leftModifiers &= ~ES_MODIFIER_ALT_GR; if (scancode == ES_SCANCODE_RIGHT_FLAG) leftModifiers |= ES_MODIFIER_FLAG; if (scancode == (ES_SCANCODE_RIGHT_FLAG | K_SCANCODE_KEY_RELEASED)) leftModifiers &= ~ES_MODIFIER_FLAG; if (scancode == ES_SCANCODE_NUM_LOCK) { numlock = !numlock; HIDeviceUpdateIndicators(); } EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); message.type = (scancode & K_SCANCODE_KEY_RELEASED) ? ES_MSG_KEY_UP : ES_MSG_KEY_DOWN; message.keyboard.modifiers = leftModifiers | rightModifiers; message.keyboard.scancode = scancode & ~K_SCANCODE_KEY_RELEASED; message.keyboard.numlock = numlock; if (message.keyboard.scancode >= 512) { KernelPanic("WindowManager::PressKey - Scancode outside valid range.\n"); } if (message.type == ES_MSG_KEY_DOWN && (keysHeldBitSet[message.keyboard.scancode / 8] & (1 << (message.keyboard.scancode % 8)))) { message.keyboard.repeat = true; } if (message.type == ES_MSG_KEY_DOWN) { keysHeldBitSet[message.keyboard.scancode / 8] |= (1 << (message.keyboard.scancode % 8)); } else { keysHeldBitSet[message.keyboard.scancode / 8] &= ~(1 << (message.keyboard.scancode % 8)); } message.keyboard.single = (scancode & K_SCANCODE_KEY_RELEASED) && maximumKeysHeld == 1; if (!message.keyboard.repeat) keysHeld += (scancode & K_SCANCODE_KEY_RELEASED) ? -1 : 1; keysHeld = MaximumInteger(keysHeld, 0); // Prevent negative keys held count. maximumKeysHeld = (!keysHeld || keysHeld > maximumKeysHeld) ? keysHeld : maximumKeysHeld; KernelLog(LOG_VERBOSE, "WM", "press key", "WindowManager::PressKey - Received key press %x. Modifiers are %X. Keys held: %d/%d%z.\n", scancode, leftModifiers | rightModifiers, keysHeld, maximumKeysHeld, message.keyboard.single ? " (single)" : ""); if ((((leftModifiers | rightModifiers) & ES_MODIFIER_CTRL) && ((leftModifiers | rightModifiers) & ES_MODIFIER_FLAG)) || !activeWindow) { _EsMessageWithObject messageWithObject = { nullptr, message }; DesktopSendMessage(&messageWithObject); } else { SendMessageToWindow(activeWindow, &message); } KMutexRelease(&mutex); } Window *WindowManager::FindWindowToActivate(Window *excluding) { for (uintptr_t i = windows.Length(); i > 0; i--) { if (!windows[i - 1]->hidden && windows[i - 1]->style == ES_WINDOW_CONTAINER && windows[i - 1] != excluding) { return windows[i - 1]; } } return nullptr; } uintptr_t WindowManager::GetActivationZIndex() { for (uintptr_t i = windows.Length(); i > 0; i--) { if (!windows[i - 1]->alwaysOnTop) { return i; } } return 0; } void WindowManager::HideWindow(Window *window) { KMutexAssertLocked(&mutex); if (window->hidden) return; window->hidden = true; if (window == activeWindow) { ActivateWindow(FindWindowToActivate()); } Redraw(ES_POINT(window->position.x, window->position.y), window->width, window->height); } intptr_t WindowManager::FindWindowDepth(Window *window) { KMutexAssertLocked(&windowManager.mutex); for (uintptr_t i = 0; i < windows.Length(); i++) { if (windows[i] == window) { return i; } } return -1; } void WindowManager::ChangeWindowDepth(Window *window, bool redraw, ptrdiff_t _newZDepth) { KMutexAssertLocked(&windowManager.mutex); // Reorder the windows in the array, and update the depth buffer. intptr_t oldZDepth = FindWindowDepth(window); if (oldZDepth == -1) KernelPanic("WindowManager::ChangeWindowDepth - Window %x was not in array.\n", window); windows.Delete(oldZDepth); intptr_t newZDepth = _newZDepth != -1 ? _newZDepth : GetActivationZIndex(); windows.Insert(window, newZDepth); if (oldZDepth != newZDepth || redraw) { // Redraw the modified area of the screen. windowManager.Redraw(ES_POINT(window->position.x, window->position.y), window->width, window->height); } } bool WindowManager::CloseMenus() { KMutexAssertLocked(&mutex); EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); bool result = false; for (uintptr_t i = 0; i < windows.Length(); i++) { // Close any open menus. if (windows[i]->style == ES_WINDOW_MENU) { message.type = ES_MSG_WINDOW_DEACTIVATED; SendMessageToWindow(windows[i], &message); result = true; } } return result; } bool WindowManager::ActivateWindow(Window *window) { KMutexAssertLocked(&mutex); if (window && (window->style == ES_WINDOW_TIP || window->style == ES_WINDOW_MENU || window->closed)) { return false; } Window *oldWindow = activeWindow; EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); bool result = CloseMenus(); // Set the active window, unless it hasn't changed. if (activeWindow == window && (!window || !window->hidden)) { // Bring the window to the front anyway. if (window && !window->noBringToFront) ChangeWindowDepth(window, false, -1); return result; } activeWindow = window; if (window) window->hidden = false; if (window) { // Bring the window to the front. if (!window->noBringToFront) ChangeWindowDepth(window, true, -1); // Activate the new window. message.type = ES_MSG_WINDOW_ACTIVATED; message.windowActivated.leftModifiers = leftModifiers; message.windowActivated.rightModifiers = rightModifiers; SendMessageToWindow(window, &message); } else { // No window is active. } if (oldWindow && oldWindow != window) { // Deactivate the old window. if (oldWindow != FindWindowAtPosition(cursorX, cursorY)) { message.type = ES_MSG_MOUSE_EXIT; SendMessageToWindow(oldWindow, &message); } message.type = ES_MSG_WINDOW_DEACTIVATED; SendMessageToWindow(oldWindow, &message); } return result; } void WindowManager::ClickCursor(uint32_t buttons) { KMutexAcquire(&mutex); unsigned delta = lastButtons ^ buttons; lastButtons = buttons; bool moveCursorNone = false; if (eyedropping && delta) { if ((delta & K_LEFT_BUTTON) && (~buttons & K_LEFT_BUTTON)) { EndEyedrop(false); moveCursorNone = true; } } else if (delta) { // Send a mouse pressed message to the window the cursor is over. Window *window = FindWindowAtPosition(cursorX, cursorY); bool activationClick = false, activationClickFromMenu = false; if (buttons == delta) { if (activeWindow != window) { activationClick = true; } if (window && window->noClickActivate) { if (!window->noBringToFront) { ChangeWindowDepth(window, false, -1); } if (window->style != ES_WINDOW_MENU) { activationClickFromMenu = CloseMenus(); } } else { activationClickFromMenu = ActivateWindow(window); } } { EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); if (delta & K_LEFT_BUTTON) message.type = (buttons & K_LEFT_BUTTON) ? ES_MSG_MOUSE_LEFT_DOWN : ES_MSG_MOUSE_LEFT_UP; if (delta & K_RIGHT_BUTTON) message.type = (buttons & K_RIGHT_BUTTON) ? ES_MSG_MOUSE_RIGHT_DOWN : ES_MSG_MOUSE_RIGHT_UP; if (delta & K_MIDDLE_BUTTON) message.type = (buttons & K_MIDDLE_BUTTON) ? ES_MSG_MOUSE_MIDDLE_DOWN : ES_MSG_MOUSE_MIDDLE_UP; // TODO Setting pressedWindow if holding with other mouse buttons. if (message.type == ES_MSG_MOUSE_LEFT_DOWN || message.type == ES_MSG_MOUSE_MIDDLE_DOWN || message.type == ES_MSG_MOUSE_RIGHT_DOWN) { if (!pressedWindow) { pressedWindowButton = message.type; pressedWindow = window; } } if (message.type == ES_MSG_MOUSE_LEFT_UP || message.type == ES_MSG_MOUSE_MIDDLE_UP || message.type == ES_MSG_MOUSE_RIGHT_UP) { if (pressedWindow) { // Always send the messages to the pressed window, if there is one. window = pressedWindow; } if (pressedWindowButton == message.type - 1) { // Only end pressing if this is the same button as pressing started with. pressedWindow = nullptr; moveCursorNone = true; // We might have moved outside the window. } } if (window) { message.mouseDown.positionX = cursorX - window->position.x; message.mouseDown.positionY = cursorY - window->position.y; window->activationClick = activationClick || activationClickFromMenu; SendMessageToWindow(window, &message); } } } if (moveCursorNone) { MoveCursor(0, 0); } else { GraphicsUpdateScreen(); } KMutexRelease(&mutex); } void WindowManager::MoveCursor(int64_t xMovement, int64_t yMovement) { KMutexAssertLocked(&mutex); if ((xMovement * xMovement + yMovement * yMovement > 50 * K_CURSOR_MOVEMENT_SCALE * K_CURSOR_MOVEMENT_SCALE) && (cursorProperties & CURSOR_USE_ACCELERATION)) { // Apply cursor acceleration. xMovement *= 2; yMovement *= 2; } // Apply cursor speed. Introduces another factor of K_CURSOR_MOVEMENT_SCALE. xMovement *= CURSOR_SPEED(cursorProperties); yMovement *= CURSOR_SPEED(cursorProperties); if ((leftModifiers & ES_MODIFIER_ALT) && (cursorProperties & CURSOR_USE_ALT_SLOW)) { // Apply cursor slowdown. xMovement /= 5, yMovement /= 5; } // Update cursor position. cursorXPrecise = ClampInteger(0, graphics.width * K_CURSOR_MOVEMENT_SCALE - 1, cursorXPrecise + xMovement / K_CURSOR_MOVEMENT_SCALE); cursorYPrecise = ClampInteger(0, graphics.height * K_CURSOR_MOVEMENT_SCALE - 1, cursorYPrecise + yMovement / K_CURSOR_MOVEMENT_SCALE); cursorX = cursorXPrecise / K_CURSOR_MOVEMENT_SCALE; cursorY = cursorYPrecise / K_CURSOR_MOVEMENT_SCALE; if (eyedropping) { EsMessage m = { ES_MSG_EYEDROP_REPORT }; uint32_t color = *(uint32_t *) ((uint8_t *) graphics.frameBuffer.bits + 4 * cursorX + graphics.frameBuffer.stride * cursorY); m.eyedrop.color = color; m.eyedrop.cancelled = false; eyedropProcess->messageQueue.SendMessage((void *) eyedropObject, &m); } else { Window *window = pressedWindow; if (!window) { // Work out which window the mouse is now over. window = FindWindowAtPosition(cursorX, cursorY); if (hoverWindow != window && hoverWindow) { EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); message.type = ES_MSG_MOUSE_EXIT; SendMessageToWindow(hoverWindow, &message); } hoverWindow = window; } if (window) { EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); message.type = ES_MSG_MOUSE_MOVED; message.mouseMoved.newPositionX = cursorX - window->position.x; message.mouseMoved.newPositionY = cursorY - window->position.y; SendMessageToWindow(window, &message); } } GraphicsUpdateScreen(); } void WindowManager::ScrollWheel(int32_t dx, int32_t dy) { KMutexAssertLocked(&mutex); Window *window = pressedWindow ?: FindWindowAtPosition(cursorX, cursorY); if (!window) return; EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); message.type = ES_MSG_SCROLL_WHEEL; message.scrollWheel.dx = dx; message.scrollWheel.dy = dy; SendMessageToWindow(window, &message); } void _CloseWindows(uintptr_t) { while (true) { KEventWait(&windowManager.windowsToCloseEvent); KMutexAcquire(&windowManager.mutex); for (uintptr_t i = 0; i < windowManager.windows.Length(); i++) { Window *window = windowManager.windows[i]; if (window->handles > 1) { continue; } // Remove the window from the array. windowManager.windows.Delete(i); if (!window->closed) { // Only the window manager's handle to the window remains, // but the window has not been closed. // This probably means the process crashed before it could close its windows; // we should close the window ourselves. window->Close(); } // Close the window manager's handle to the window. CloseHandleToObject(window, KERNEL_OBJECT_WINDOW); i--; } for (uintptr_t i = 0; i < windowManager.embeddedWindows.Length(); i++) { // Apply a similar set of operations to embedded windows. EmbeddedWindow *window = windowManager.embeddedWindows[i]; if (window->handles > 1) continue; if (!window->closed) window->Close(); windowManager.embeddedWindows.Delete(i); CloseHandleToObject(window, KERNEL_OBJECT_EMBEDDED_WINDOW); i--; } KMutexRelease(&windowManager.mutex); } } void WindowManager::Initialise() { windowsToCloseEvent.autoReset = true; cursorProperties = K_CURSOR_MOVEMENT_SCALE << 16; KThreadCreate("CloseWindows", _CloseWindows); KMutexAcquire(&mutex); MoveCursor(graphics.width / 2 * K_CURSOR_MOVEMENT_SCALE, graphics.height / 2 * K_CURSOR_MOVEMENT_SCALE); GraphicsUpdateScreen(); initialised = true; KMutexRelease(&mutex); } void *WindowManager::CreateEmbeddedWindow(Process *process, void *apiWindow) { EmbeddedWindow *window = (EmbeddedWindow *) EsHeapAllocate(sizeof(EmbeddedWindow), true, K_PAGED); if (!window) return nullptr; if (!embeddedWindows.Add(window)) { EsHeapFree(window, sizeof(EmbeddedWindow), K_PAGED); return nullptr; } window->apiWindow = apiWindow; window->owner = process; window->handles = 2; // One handle for the window manager, and one handle for the calling process. KMutexAcquire(&mutex); window->id = ++currentWindowID; KMutexRelease(&mutex); OpenHandleToObject(window->owner, KERNEL_OBJECT_PROCESS); return window; } void *WindowManager::CreateWindow(Process *process, void *apiWindow, EsWindowStyle style) { KMutexAcquire(&mutex); EsDefer(KMutexRelease(&mutex)); // Allocate and initialise the window object. Window *window = (Window *) EsHeapAllocate(sizeof(Window), true, K_PAGED); if (!window) return nullptr; window->style = style; window->apiWindow = apiWindow; window->alpha = 0xFF; window->position = ES_POINT(-8, -8); window->owner = process; window->handles = 2; // One handle for the window manager, and one handle for the calling process. window->hidden = true; window->id = ++currentWindowID; if (style == ES_WINDOW_INSPECTOR) windowManager.inspectorWindowCount++; // Insert the window into the window array. uintptr_t insertionPoint = style == ES_WINDOW_TIP ? windows.Length() : GetActivationZIndex(); if (!windows.Insert(window, insertionPoint)) { EsHeapFree(window->surface.bits, 0, K_PAGED); EsHeapFree(window, sizeof(Window), K_PAGED); return nullptr; } // Get a handle to the owner process. OpenHandleToObject(window->owner, KERNEL_OBJECT_PROCESS); MoveCursor(0, 0); return window; } bool Window::Move(EsRectangle rectangle, uint32_t flags) { KMutexAssertLocked(&windowManager.mutex); if (closed) { return false; } if ((flags & ES_WINDOW_MOVE_DYNAMIC) && (~flags & ES_WINDOW_MOVE_MAXIMIZED) && !isMaximised && windowManager.resizeWindow == this && windowManager.resizeStartTimeStampMs + RESIZE_FLICKER_TIMEOUT_MS > KGetTimeInMs()) { windowManager.resizeQueued = true; windowManager.resizeQueuedRectangle = rectangle; return false; } windowManager.resizeQueued = false; bool result = true; isMaximised = flags & ES_WINDOW_MOVE_MAXIMIZED; alwaysOnTop = flags & ES_WINDOW_MOVE_ALWAYS_ON_TOP; // TS("Move window\n"); if (flags & ES_WINDOW_MOVE_ADJUST_TO_FIT_SCREEN) { rectangle = EsRectangleAdd(rectangle, solidOffsets); if (rectangle.r > (int32_t) graphics.frameBuffer.width) { rectangle = Translate(rectangle, graphics.frameBuffer.width - rectangle.r, 0); } if (rectangle.b > (int32_t) graphics.frameBuffer.height) { rectangle = Translate(rectangle, 0, graphics.frameBuffer.height - rectangle.b); } if (rectangle.l < 0) { rectangle = Translate(rectangle, -rectangle.l, 0); } if (rectangle.t < 0) { rectangle = Translate(rectangle, 0, -rectangle.t); } rectangle = EsRectangleSubtract(rectangle, solidOffsets); } size_t newWidth = rectangle.r - rectangle.l; size_t newHeight = rectangle.b - rectangle.t; if (newWidth < 4 || newHeight < 4 || rectangle.l > rectangle.r || rectangle.t > rectangle.b || newWidth > graphics.width * 2 || newHeight > graphics.height * 2) { return false; } if (!hidden) { // TS("Clear previous image\n"); windowManager.Redraw(ES_POINT(position.x, position.y), width, height, this); } hidden = false; position = ES_POINT(rectangle.l, rectangle.t); bool changedSize = width != newWidth || height != newHeight; width = newWidth, height = newHeight; if (changedSize) { if (style == ES_WINDOW_CONTAINER && embed) { surface.Resize(width, height, 0, windowManager.resizeSlow); } else { surface.Resize(width, height); } EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); message.type = ES_MSG_WINDOW_RESIZED; message.windowResized.content = ES_RECT_4(0, width, 0, height); SendMessageToWindow(this, &message); if (flags & ES_WINDOW_MOVE_DYNAMIC) { // Don't redraw the screen until both the container and embedded window have painted // (or the timeout completes). windowManager.resizeWindow = this; windowManager.resizeReceivedBitsFromContainer = false; windowManager.resizeReceivedBitsFromEmbed = embed == nullptr; windowManager.resizeStartTimeStampMs = KGetTimeInMs(); } } if (flags & ES_WINDOW_MOVE_AT_BOTTOM) { windowManager.ChangeWindowDepth(this, false, 0); } if ((flags & ES_WINDOW_MOVE_DYNAMIC) && changedSize && style == ES_WINDOW_CONTAINER && !windowManager.resizeSlow) { // Don't redraw anything yet. } else { windowManager.Redraw(position, width, height, nullptr); } return result; } void EmbeddedWindow::Destroy() { KernelLog(LOG_INFO, "WM", "destroy embedded window", "EmbeddedWindow::Destroy - Destroying embedded window.\n"); EsHeapFree(this, sizeof(EmbeddedWindow), K_PAGED); } void EmbeddedWindow::Close() { KMutexAssertLocked(&windowManager.mutex); _EsMessageWithObject m; EsMemoryZero(&m, sizeof(m)); m.object = apiWindow; m.message.type = ES_MSG_WINDOW_DESTROYED; owner->messageQueue.SendMessage(&m); SetEmbedOwner(nullptr); m.object = nullptr; m.message.type = ES_MSG_EMBEDDED_WINDOW_DESTROYED; m.message.embeddedWindowDestroyedID = id; DesktopSendMessage(&m); if (container && container->embed == this) { container->SetEmbed(nullptr); } closed = true; } void Window::Destroy() { if (!closed) { KernelPanic("Window::Destroy - Window %x has not been closed.\n", this); } EsHeapFree(this, sizeof(Window), K_PAGED); } void Window::Close() { KMutexAssertLocked(&windowManager.mutex); SetEmbed(nullptr); // Send the destroy message - the last message sent to the window. EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); message.type = ES_MSG_WINDOW_DESTROYED; owner->messageQueue.SendMessage(apiWindow, &message); Process *_owner = owner; owner = nullptr; CloseHandleToObject(_owner, KERNEL_OBJECT_PROCESS); hidden = true; solid = false; bool findActiveWindow = false; if (windowManager.pressedWindow == this) windowManager.pressedWindow = nullptr; if (windowManager.hoverWindow == this) windowManager.hoverWindow = nullptr; if (windowManager.resizeWindow == this) windowManager.resizeWindow = nullptr; if (windowManager.activeWindow == this) windowManager.activeWindow = nullptr, findActiveWindow = true; if (style == ES_WINDOW_INSPECTOR) windowManager.inspectorWindowCount--; windowManager.Redraw(ES_POINT(position.x, position.y), width, height); if (findActiveWindow) { windowManager.ActivateWindow(windowManager.FindWindowToActivate()); } windowManager.MoveCursor(0, 0); GraphicsUpdateScreen(); __sync_fetch_and_sub(&graphics.totalSurfaceBytes, surface.width * surface.height * 4); EsHeapFree(surface.bits, 0, K_PAGED); surface.bits = nullptr; __sync_synchronize(); closed = true; } void WindowManager::Redraw(EsPoint position, int width, int height, Window *except, int startingAt, bool addToModifiedRegion) { // TS("Window manager redraw at [%d, %d] with size [%d, %d]\n", position.x, position.y, width, height); KMutexAssertLocked(&mutex); if (!width || !height) return; if (scheduler.shutdown) return; for (int index = startingAt; index < (int) windows.Length(); index++) { Window *window = windows[index]; if (!window->IsVisible() || window == except) { continue; } if (position.x >= window->position.x + window->opaqueBounds.l && position.x + width <= window->position.x + window->opaqueBounds.r && position.y >= window->position.y + window->opaqueBounds.t && position.y + height <= window->position.y + window->opaqueBounds.b) { startingAt = index; } } for (int index = startingAt; index < (int) windows.Length(); index++) { Window *window = windows[index]; if (!window->IsVisible() || window == except) continue; EsRectangle rectangle = ES_RECT_4PD(window->position.x, window->position.y, window->width, window->height); rectangle = EsRectangleIntersection(rectangle, ES_RECT_2S(graphics.frameBuffer.width, graphics.frameBuffer.height)); rectangle = EsRectangleIntersection(rectangle, ES_RECT_4PD(position.x, position.y, width, height)); if (window->isMaximised) rectangle = EsRectangleIntersection(rectangle, workArea); rectangle = Translate(rectangle, -window->position.x, -window->position.y); if (!ES_RECT_VALID(rectangle)) continue; Surface *surface = &window->surface; EsRectangle transparentRegions[] = { EsRectangleIntersection(rectangle, ES_RECT_4(0, window->opaqueBounds.l, 0, window->height)), EsRectangleIntersection(rectangle, ES_RECT_4(window->opaqueBounds.r, window->width, 0, window->height)), EsRectangleIntersection(rectangle, ES_RECT_4(window->opaqueBounds.l, window->opaqueBounds.r, 0, window->opaqueBounds.t)), EsRectangleIntersection(rectangle, ES_RECT_4(window->opaqueBounds.l, window->opaqueBounds.r, window->opaqueBounds.b, window->height)), }; EsRectangle opaqueRegion = EsRectangleIntersection(rectangle, window->opaqueBounds); EsRectangle blurRegion = Translate(EsRectangleIntersection(window->blurBounds, rectangle), window->position.x, window->position.y); for (uintptr_t i = 0; i < 4; i++) { if (ES_RECT_VALID(transparentRegions[i])) { EsPoint point = ES_POINT(window->position.x + transparentRegions[i].l, window->position.y + transparentRegions[i].t); graphics.frameBuffer.BlendWindow(surface, point, transparentRegions[i], window->material, window->alpha, blurRegion); } } if (ES_RECT_VALID(opaqueRegion)) { EsPoint point = ES_POINT(window->position.x + opaqueRegion.l, window->position.y + opaqueRegion.t); graphics.frameBuffer.Copy(surface, point, opaqueRegion, addToModifiedRegion); } } } bool Window::UpdateDirect(K_USER_BUFFER void *bits, uintptr_t stride, EsRectangle region) { KMutexAssertLocked(&windowManager.mutex); intptr_t z = windowManager.FindWindowDepth(this); if (z == -1) { return false; } if (hidden) { return false; } if (windowManager.changedCursorImage) { // The entire cursor needs to be redrawn about its image changes, // which might not happen with a direct update. return false; } if (region.l + position.x < 0) { bits = (K_USER_BUFFER uint8_t *) bits + 4 * (-position.x - region.l); region.l = -position.x; } if (region.t + position.y < 0) { bits = (K_USER_BUFFER uint8_t *) bits + stride * (-position.y - region.t); region.t = -position.y; } if ((uint32_t) (region.r + position.x) > graphics.width) { region.r = graphics.width - position.x; } if ((uint32_t) (region.b + position.y) > graphics.height) { region.b = graphics.height - position.y; } // If the update region is completely obscured, then we don't need to update. for (uintptr_t i = z + 1; i < windowManager.windows.Length(); i++) { Window *other = windowManager.windows[i]; EsRectangle otherBounds = Translate(other->opaqueBounds, other->position.x, other->position.y); if (region.l + position.x > otherBounds.l && region.r + position.x < otherBounds.r && region.t + position.y > otherBounds.t && region.b + position.y < otherBounds.b && other->alpha == 0xFF && other->IsVisible()) { return true; } } // If the update region isn't opaque, we cannot do a direct update. if (region.l < opaqueBounds.l || region.r > opaqueBounds.r || region.t < opaqueBounds.t || region.b > opaqueBounds.b || alpha != 0xFF) { return false; } // If any window overlaps the update region, we cannot do a direct update. EsRectangle thisBounds = ES_RECT_4(position.x, position.x + width, position.y, position.y + height); for (uintptr_t i = z + 1; i < windowManager.windows.Length(); i++) { Window *other = windowManager.windows[i]; EsRectangle otherBounds = ES_RECT_4(other->position.x, other->position.x + other->width, other->position.y, other->position.y + other->height); if (EsRectangleClip(thisBounds, otherBounds, nullptr)) { return false; } } region = Translate(region, position.x, position.y); // Write the updated bits directly to the frame buffer! GraphicsUpdateScreen(bits, ®ion, stride); return true; } void Window::Update(EsRectangle *_region, bool addToModifiedRegion) { KMutexAssertLocked(&windowManager.mutex); intptr_t z = windowManager.FindWindowDepth(this); if (z == -1) { return; } // TS("Update window %x within %R\n", this, _region ? *_region : ES_RECT_1(0)); EsRectangle region = _region ? *_region : ES_RECT_4(0, width, 0, height); // Is this updated region completely obscured? for (uintptr_t i = z + 1; i < windowManager.windows.Length(); i++) { Window *other = windowManager.windows[i]; EsRectangle otherBounds = Translate(other->opaqueBounds, other->position.x, other->position.y); if (region.l + position.x > otherBounds.l && region.r + position.x < otherBounds.r && region.t + position.y > otherBounds.t && region.b + position.y < otherBounds.b && other->alpha == 0xFF && other->IsVisible()) { return; } } EsRectangle r; // EsPrint("Update window width region %d/%d/%d/%d\n", region.left, region.right, region.top, region.bottom); if (alpha == 0xFF) { EsRectangle translucentBorder = opaqueBounds; // Draw the opaque region, then everything above it. EsRectangleClip(region, ES_RECT_4(translucentBorder.l, translucentBorder.r, translucentBorder.t, translucentBorder.b), &r); windowManager.Redraw(ES_POINT(position.x + r.l, position.y + r.t), r.r - r.l, r.b - r.t, nullptr, z, addToModifiedRegion); // Draw the transparent regions after everything below them, then everything above them. EsRectangleClip(region, ES_RECT_4(0, translucentBorder.l, 0, height), &r); windowManager.Redraw(ES_POINT(position.x + r.l, position.y + r.t), r.r - r.l, r.b - r.t, nullptr, 0, addToModifiedRegion); EsRectangleClip(region, ES_RECT_4(translucentBorder.r, width, 0, height), &r); windowManager.Redraw(ES_POINT(position.x + r.l, position.y + r.t), r.r - r.l, r.b - r.t, nullptr, 0, addToModifiedRegion); EsRectangleClip(region, ES_RECT_4(translucentBorder.l, translucentBorder.r, 0, translucentBorder.t), &r); windowManager.Redraw(ES_POINT(position.x + r.l, position.y + r.t), r.r - r.l, r.b - r.t, nullptr, 0, addToModifiedRegion); EsRectangleClip(region, ES_RECT_4(translucentBorder.l, translucentBorder.r, translucentBorder.b, height), &r); windowManager.Redraw(ES_POINT(position.x + r.l, position.y + r.t), r.r - r.l, r.b - r.t, nullptr, 0, addToModifiedRegion); } else { // The whole window is translucent; draw it. EsRectangleClip(region, ES_RECT_4(0, width, 0, height), &r); windowManager.Redraw(ES_POINT(position.x + r.l, position.y + r.t), r.r - r.l, r.b - r.t, nullptr, 0, addToModifiedRegion); } } void EmbeddedWindow::SetEmbedOwner(Process *process) { KMutexAssertLocked(&windowManager.mutex); apiWindow = nullptr; if (process) { OpenHandleToObject(process, KERNEL_OBJECT_PROCESS); } if (owner) { CloseHandleToObject(owner, KERNEL_OBJECT_PROCESS); } owner = process; } void Window::ResizeEmbed() { KMutexAssertLocked(&windowManager.mutex); if (!embed || !embed->apiWindow) return; EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); message.type = ES_MSG_WINDOW_RESIZED; message.windowResized.content = ES_RECT_2S(width - embedInsets.l - embedInsets.r, height - embedInsets.t - embedInsets.b); embed->owner->messageQueue.SendMessage(embed->apiWindow, &message); } void Window::SetEmbed(EmbeddedWindow *newEmbed) { KMutexAssertLocked(&windowManager.mutex); if (newEmbed && (newEmbed->container || newEmbed->closed)) { return; } if (newEmbed == embed) { return; } if (embed) { if (embed->closed) { KernelPanic("Window::SetEmbed - Previous embed %x was closed.\n"); } embed->container = nullptr; EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); message.type = ES_MSG_WINDOW_RESIZED; message.windowResized.content = ES_RECT_4(0, 0, 0, 0); message.windowResized.hidden = true; if (embed->owner) { embed->owner->messageQueue.SendMessage(embed->apiWindow, &message); } } embed = newEmbed; if (embed) { embed->container = this; ResizeEmbed(); EsMessage message; EsMemoryZero(&message, sizeof(message)); message.type = windowManager.activeWindow == this ? ES_MSG_WINDOW_ACTIVATED : ES_MSG_WINDOW_DEACTIVATED; if (message.type == ES_MSG_WINDOW_ACTIVATED) { message.windowActivated.leftModifiers = windowManager.leftModifiers; message.windowActivated.rightModifiers = windowManager.rightModifiers; } embed->owner->messageQueue.SendMessage(embed->apiWindow, &message); } windowManager.CloseMenus(); } void WindowManager::StartEyedrop(uintptr_t object, Window *avoid, uint32_t cancelColor) { KMutexAcquire(&mutex); if (!eyedropping) { eyedropObject = object; eyedropping = true; eyedropAvoidID = avoid->id; eyedropCancelColor = cancelColor; Redraw(avoid->position, avoid->width, avoid->height); if (hoverWindow) { EsMessage message; EsMemoryZero(&message, sizeof(EsMessage)); message.type = ES_MSG_MOUSE_EXIT; SendMessageToWindow(hoverWindow, &message); } hoverWindow = pressedWindow = nullptr; eyedropProcess = GetCurrentThread()->process; OpenHandleToObject(eyedropProcess, KERNEL_OBJECT_PROCESS); } GraphicsUpdateScreen(); KMutexRelease(&mutex); } void KMouseUpdate(const KMouseUpdateData *data) { if (!windowManager.initialised) { return; } KMutexAcquire(&windowManager.mutex); if (data->xMovement || data->yMovement) { int32_t xMovement = data->xMovement; int32_t yMovement = data->yMovement; // TODO Handling xIsAbsolute and yIsAbsolute if (xMovement * xMovement + yMovement * yMovement < 10 && data->buttons != windowManager.lastButtons) { // This seems to be movement noise generated when the buttons were pressed/released. } else { windowManager.MoveCursor(xMovement, yMovement); } } if (data->xScroll || data->yScroll) { windowManager.ScrollWheel(data->xScroll, data->yScroll); } KMutexRelease(&windowManager.mutex); windowManager.ClickCursor(data->buttons); } void KKeyPress(uint32_t scancode) { windowManager.PressKey(scancode); } void KKeyboardUpdate(uint16_t *keysDown, size_t keysDownCount) { // TODO Key repeat. static uint16_t previousKeysDown[32] = {}; static size_t previousKeysDownCount = 0; static KMutex mutex = {}; KMutexAcquire(&mutex); EsDefer(KMutexRelease(&mutex)); if (keysDownCount > 32) { keysDownCount = 32; } for (uintptr_t i = 0; i < keysDownCount; i++) { bool found = false; for (uintptr_t j = 0; j < previousKeysDownCount; j++) { if (keysDown[i] == previousKeysDown[j]) { found = true; break; } } if (!found && keysDown[i]) { if (keysDown[i] == ES_SCANCODE_PAUSE) { KDebugKeyPressed(); // TODO Doesn't work if scheduler not functioning correctly. } KKeyPress(keysDown[i] | K_SCANCODE_KEY_PRESSED); } } for (uintptr_t i = 0; i < previousKeysDownCount; i++) { bool found = false; for (uintptr_t j = 0; j < keysDownCount; j++) { if (keysDown[j] == previousKeysDown[i]) { found = true; break; } } if (!found) { KKeyPress(previousKeysDown[i] | K_SCANCODE_KEY_RELEASED); } } previousKeysDownCount = keysDownCount; EsMemoryCopy(previousKeysDown, keysDown, sizeof(uint16_t) * keysDownCount); } uint64_t KGameControllerConnect() { KMutexAcquire(&windowManager.deviceMutex); EsObjectID id = ++windowManager.gameControllerID; if (windowManager.gameControllerCount != ES_GAME_CONTROLLER_MAX_COUNT) { windowManager.gameControllers[windowManager.gameControllerCount++].id = id; } else { id = 0; } KMutexRelease(&windowManager.deviceMutex); return id; } void KGameControllerDisconnect(uint64_t id) { KMutexAcquire(&windowManager.deviceMutex); for (uintptr_t i = 0; i < windowManager.gameControllerCount; i++) { if (windowManager.gameControllers[i].id == id) { EsMemoryMove(windowManager.gameControllers + i + 1, windowManager.gameControllers + windowManager.gameControllerCount, -sizeof(EsGameControllerState), false); windowManager.gameControllerCount--; break; } } KMutexRelease(&windowManager.deviceMutex); } void KGameControllerUpdate(EsGameControllerState *state) { KMutexAcquire(&windowManager.deviceMutex); for (uintptr_t i = 0; i < windowManager.gameControllerCount; i++) { if (windowManager.gameControllers[i].id == state->id) { windowManager.gameControllers[i] = *state; #if 0 EsPrint("game controller %d: buttons %x analog %d %d %d %d %d %d dpad %d\n", state->id, state->buttons, state->analog[0].x, state->analog[0].y, state->analog[0].z, state->analog[1].x, state->analog[1].y, state->analog[1].z, state->directionalPad); #endif break; } } KMutexRelease(&windowManager.deviceMutex); } #endif