diff options
Diffstat (limited to '')
-rw-r--r-- | video/out/w32_common.c | 2144 |
1 files changed, 2144 insertions, 0 deletions
diff --git a/video/out/w32_common.c b/video/out/w32_common.c new file mode 100644 index 0000000..e6a4670 --- /dev/null +++ b/video/out/w32_common.c @@ -0,0 +1,2144 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <limits.h> +#include <stdatomic.h> +#include <stdio.h> + +#include <windows.h> +#include <windowsx.h> +#include <dwmapi.h> +#include <ole2.h> +#include <shobjidl.h> +#include <avrt.h> + +#include "options/m_config.h" +#include "options/options.h" +#include "input/keycodes.h" +#include "input/input.h" +#include "input/event.h" +#include "stream/stream.h" +#include "common/msg.h" +#include "common/common.h" +#include "vo.h" +#include "win_state.h" +#include "w32_common.h" +#include "win32/displayconfig.h" +#include "win32/droptarget.h" +#include "osdep/io.h" +#include "osdep/threads.h" +#include "osdep/w32_keyboard.h" +#include "misc/dispatch.h" +#include "misc/rendezvous.h" +#include "mpv_talloc.h" + +EXTERN_C IMAGE_DOS_HEADER __ImageBase; +#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase) + +#ifndef WM_DPICHANGED +#define WM_DPICHANGED (0x02E0) +#endif + +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + + +//Older MinGW compatibility +#define DWMWA_WINDOW_CORNER_PREFERENCE 33 +#define DWMWA_SYSTEMBACKDROP_TYPE 38 + +#ifndef DPI_ENUMS_DECLARED +typedef enum MONITOR_DPI_TYPE { + MDT_EFFECTIVE_DPI = 0, + MDT_ANGULAR_DPI = 1, + MDT_RAW_DPI = 2, + MDT_DEFAULT = MDT_EFFECTIVE_DPI +} MONITOR_DPI_TYPE; +#endif + +#define rect_w(r) ((r).right - (r).left) +#define rect_h(r) ((r).bottom - (r).top) + +struct w32_api { + HRESULT (WINAPI *pGetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); + BOOL (WINAPI *pImmDisableIME)(DWORD); + BOOL (WINAPI *pAdjustWindowRectExForDpi)(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi); + BOOLEAN (WINAPI *pShouldAppsUseDarkMode)(void); + DWORD (WINAPI *pSetPreferredAppMode)(DWORD mode); +}; + +struct vo_w32_state { + struct mp_log *log; + struct vo *vo; + struct mp_vo_opts *opts; + struct m_config_cache *opts_cache; + struct input_ctx *input_ctx; + + mp_thread thread; + bool terminate; + struct mp_dispatch_queue *dispatch; // used to run stuff on the GUI thread + bool in_dispatch; + + struct w32_api api; // stores functions from dynamically loaded DLLs + + HWND window; + HWND parent; // 0 normally, set in embedding mode + HHOOK parent_win_hook; + HWINEVENTHOOK parent_evt_hook; + + HMONITOR monitor; // Handle of the current screen + char *color_profile; // Path of the current screen's color profile + + // Has the window seen a WM_DESTROY? If so, don't call DestroyWindow again. + bool destroyed; + + bool focused; + + // whether the window position and size were initialized + bool window_bounds_initialized; + + bool current_fs; + bool toggle_fs; // whether the current fullscreen state needs to be switched + + // Note: maximized state doesn't involve nor modify windowrc + RECT windowrc; // currently known normal/fullscreen window client rect + RECT prev_windowrc; // saved normal window client rect while in fullscreen + + // video size + uint32_t o_dwidth; + uint32_t o_dheight; + + int dpi; + double dpi_scale; + + bool disable_screensaver; + bool cursor_visible; + atomic_uint event_flags; + + BOOL tracking; + TRACKMOUSEEVENT trackEvent; + + int mouse_x; + int mouse_y; + + // Should SetCursor be called when handling VOCTRL_SET_CURSOR_VISIBILITY? + bool can_set_cursor; + + // UTF-16 decoding state for WM_CHAR and VK_PACKET + int high_surrogate; + + // Fit the window to one monitor working area next time it's not fullscreen + // and not maximized. Used once after every new "untrusted" size comes from + // mpv, else we assume that the last known size is valid and don't fit. + // FIXME: on a multi-monitor setup one bit is not enough, because the first + // fit (autofit etc) should be to one monitor, but later size changes from + // mpv like window-scale (VOCTRL_SET_UNFS_WINDOW_SIZE) should allow the + // entire virtual desktop area - but we still limit to one monitor size. + bool fit_on_screen; + + bool win_force_pos; + + ITaskbarList2 *taskbar_list; + ITaskbarList3 *taskbar_list3; + UINT tbtnCreatedMsg; + bool tbtnCreated; + + struct voctrl_playback_state current_pstate; + + // updates on move/resize/displaychange + double display_fps; + + bool moving; + + union { + uint8_t snapped; + struct { + uint8_t snapped_left : 1; + uint8_t snapped_right : 1; + uint8_t snapped_top : 1; + uint8_t snapped_bottom : 1; + }; + }; + int snap_dx; + int snap_dy; + + HANDLE avrt_handle; + + bool cleared; +}; + +static void adjust_window_rect(struct vo_w32_state *w32, HWND hwnd, RECT *rc) +{ + if (!w32->opts->border) + return; + + if (w32->api.pAdjustWindowRectExForDpi) { + w32->api.pAdjustWindowRectExForDpi(rc, + GetWindowLongPtrW(hwnd, GWL_STYLE), 0, + GetWindowLongPtrW(hwnd, GWL_EXSTYLE), w32->dpi); + } else { + AdjustWindowRect(rc, GetWindowLongPtrW(hwnd, GWL_STYLE), 0); + } +} + +static void add_window_borders(struct vo_w32_state *w32, HWND hwnd, RECT *rc) +{ + RECT win = *rc; + adjust_window_rect(w32, hwnd, rc); + // Adjust for title bar height that will be hidden in WM_NCCALCSIZE + if (w32->opts->border && !w32->opts->title_bar && !w32->current_fs) + rc->top -= rc->top - win.top; +} + +// basically a reverse AdjustWindowRect (win32 doesn't appear to have this) +static void subtract_window_borders(struct vo_w32_state *w32, HWND hwnd, RECT *rc) +{ + RECT b = { 0, 0, 0, 0 }; + add_window_borders(w32, hwnd, &b); + rc->left -= b.left; + rc->top -= b.top; + rc->right -= b.right; + rc->bottom -= b.bottom; +} + +static LRESULT borderless_nchittest(struct vo_w32_state *w32, int x, int y) +{ + if (IsMaximized(w32->window)) + return HTCLIENT; + + RECT rc; + if (!GetWindowRect(w32->window, &rc)) + return HTNOWHERE; + + POINT frame = {GetSystemMetrics(SM_CXSIZEFRAME), + GetSystemMetrics(SM_CYSIZEFRAME)}; + if (w32->opts->border) { + frame.x += GetSystemMetrics(SM_CXPADDEDBORDER); + frame.y += GetSystemMetrics(SM_CXPADDEDBORDER); + if (!w32->opts->title_bar) + rc.top -= GetSystemMetrics(SM_CXPADDEDBORDER); + } + InflateRect(&rc, -frame.x, -frame.y); + + // Hit-test top border + if (y < rc.top) { + if (x < rc.left) + return HTTOPLEFT; + if (x > rc.right) + return HTTOPRIGHT; + return HTTOP; + } + + // Hit-test bottom border + if (y > rc.bottom) { + if (x < rc.left) + return HTBOTTOMLEFT; + if (x > rc.right) + return HTBOTTOMRIGHT; + return HTBOTTOM; + } + + // Hit-test side borders + if (x < rc.left) + return HTLEFT; + if (x > rc.right) + return HTRIGHT; + return HTCLIENT; +} + +// turn a WMSZ_* input value in v into the border that should be resized +// take into consideration which borders are snapped to avoid detaching +// returns: 0=left, 1=top, 2=right, 3=bottom, -1=undefined +static int get_resize_border(struct vo_w32_state *w32, int v) +{ + switch (v) { + case WMSZ_LEFT: + case WMSZ_RIGHT: + return w32->snapped_bottom ? 1 : 3; + case WMSZ_TOP: + case WMSZ_BOTTOM: + return w32->snapped_right ? 0 : 2; + case WMSZ_TOPLEFT: return 1; + case WMSZ_TOPRIGHT: return 1; + case WMSZ_BOTTOMLEFT: return 3; + case WMSZ_BOTTOMRIGHT: return 3; + default: return -1; + } +} + +static bool key_state(int vk) +{ + return GetKeyState(vk) & 0x8000; +} + +static int mod_state(struct vo_w32_state *w32) +{ + int res = 0; + + // AltGr is represented as LCONTROL+RMENU on Windows + bool alt_gr = mp_input_use_alt_gr(w32->input_ctx) && + key_state(VK_RMENU) && key_state(VK_LCONTROL); + + if (key_state(VK_RCONTROL) || (key_state(VK_LCONTROL) && !alt_gr)) + res |= MP_KEY_MODIFIER_CTRL; + if (key_state(VK_SHIFT)) + res |= MP_KEY_MODIFIER_SHIFT; + if (key_state(VK_LMENU) || (key_state(VK_RMENU) && !alt_gr)) + res |= MP_KEY_MODIFIER_ALT; + return res; +} + +static int decode_surrogate_pair(wchar_t lead, wchar_t trail) +{ + return 0x10000 + (((lead & 0x3ff) << 10) | (trail & 0x3ff)); +} + +static int decode_utf16(struct vo_w32_state *w32, wchar_t c) +{ + // Decode UTF-16, keeping state in w32->high_surrogate + if (IS_HIGH_SURROGATE(c)) { + w32->high_surrogate = c; + return 0; + } + if (IS_LOW_SURROGATE(c)) { + if (!w32->high_surrogate) { + MP_ERR(w32, "Invalid UTF-16 input\n"); + return 0; + } + int codepoint = decode_surrogate_pair(w32->high_surrogate, c); + w32->high_surrogate = 0; + return codepoint; + } + if (w32->high_surrogate != 0) { + w32->high_surrogate = 0; + MP_ERR(w32, "Invalid UTF-16 input\n"); + return 0; + } + + return c; +} + +static void clear_keyboard_buffer(void) +{ + static const UINT vkey = VK_DECIMAL; + static const BYTE keys[256] = { 0 }; + UINT scancode = MapVirtualKey(vkey, MAPVK_VK_TO_VSC); + wchar_t buf[10]; + int ret = 0; + + // Use the method suggested by Michael Kaplan to clear any pending dead + // keys from the current keyboard layout. See: + // https://web.archive.org/web/20101004154432/http://blogs.msdn.com/b/michkap/archive/2006/04/06/569632.aspx + // https://web.archive.org/web/20100820152419/http://blogs.msdn.com/b/michkap/archive/2007/10/27/5717859.aspx + do { + ret = ToUnicode(vkey, scancode, keys, buf, MP_ARRAY_SIZE(buf), 0); + } while (ret < 0); +} + +static int to_unicode(UINT vkey, UINT scancode, const BYTE keys[256]) +{ + // This wraps ToUnicode to be stateless and to return only one character + + // Make the buffer 10 code units long to be safe, same as here: + // https://web.archive.org/web/20101013215215/http://blogs.msdn.com/b/michkap/archive/2006/03/24/559169.aspx + wchar_t buf[10] = { 0 }; + + // Dead keys aren't useful for key shortcuts, so clear the keyboard state + clear_keyboard_buffer(); + + int len = ToUnicode(vkey, scancode, keys, buf, MP_ARRAY_SIZE(buf), 0); + + // Return the last complete UTF-16 code point. A negative return value + // indicates a dead key, however there should still be a non-combining + // version of the key in the buffer. + if (len < 0) + len = -len; + if (len >= 2 && IS_SURROGATE_PAIR(buf[len - 2], buf[len - 1])) + return decode_surrogate_pair(buf[len - 2], buf[len - 1]); + if (len >= 1) + return buf[len - 1]; + + return 0; +} + +static int decode_key(struct vo_w32_state *w32, UINT vkey, UINT scancode) +{ + BYTE keys[256]; + GetKeyboardState(keys); + + // If mp_input_use_alt_gr is false, detect and remove AltGr so normal + // characters are generated. Note that AltGr is represented as + // LCONTROL+RMENU on Windows. + if ((keys[VK_RMENU] & 0x80) && (keys[VK_LCONTROL] & 0x80) && + !mp_input_use_alt_gr(w32->input_ctx)) + { + keys[VK_RMENU] = keys[VK_LCONTROL] = 0; + keys[VK_MENU] = keys[VK_LMENU]; + keys[VK_CONTROL] = keys[VK_RCONTROL]; + } + + int c = to_unicode(vkey, scancode, keys); + + // Some shift states prevent ToUnicode from working or cause it to produce + // control characters. If this is detected, remove modifiers until it + // starts producing normal characters. + if (c < 0x20 && (keys[VK_MENU] & 0x80)) { + keys[VK_LMENU] = keys[VK_RMENU] = keys[VK_MENU] = 0; + c = to_unicode(vkey, scancode, keys); + } + if (c < 0x20 && (keys[VK_CONTROL] & 0x80)) { + keys[VK_LCONTROL] = keys[VK_RCONTROL] = keys[VK_CONTROL] = 0; + c = to_unicode(vkey, scancode, keys); + } + if (c < 0x20) + return 0; + + // Decode lone UTF-16 surrogates (VK_PACKET can generate these) + if (c < 0x10000) + return decode_utf16(w32, c); + return c; +} + +static bool handle_appcommand(struct vo_w32_state *w32, UINT cmd) +{ + if (!mp_input_use_media_keys(w32->input_ctx)) + return false; + int mpkey = mp_w32_appcmd_to_mpkey(cmd); + if (!mpkey) + return false; + mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32)); + return true; +} + +static void handle_key_down(struct vo_w32_state *w32, UINT vkey, UINT scancode) +{ + // Ignore key repeat + if (scancode & KF_REPEAT) + return; + + int mpkey = mp_w32_vkey_to_mpkey(vkey, scancode & KF_EXTENDED); + if (!mpkey) { + mpkey = decode_key(w32, vkey, scancode & (0xff | KF_EXTENDED)); + if (!mpkey) + return; + } + + mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32) | MP_KEY_STATE_DOWN); +} + +static void handle_key_up(struct vo_w32_state *w32, UINT vkey, UINT scancode) +{ + switch (vkey) { + case VK_MENU: + case VK_CONTROL: + case VK_SHIFT: + break; + default: + // Releasing all keys on key-up is simpler and ensures no keys can be + // get "stuck." This matches the behaviour of other VOs. + mp_input_put_key(w32->input_ctx, MP_INPUT_RELEASE_ALL); + } +} + +static bool handle_char(struct vo_w32_state *w32, wchar_t wc) +{ + int c = decode_utf16(w32, wc); + + if (c == 0) + return true; + if (c < 0x20) + return false; + + mp_input_put_key(w32->input_ctx, c | mod_state(w32)); + return true; +} + +static bool handle_mouse_down(struct vo_w32_state *w32, int btn, int x, int y) +{ + btn |= mod_state(w32); + mp_input_put_key(w32->input_ctx, btn | MP_KEY_STATE_DOWN); + + if (btn == MP_MBTN_LEFT && !w32->current_fs && + !mp_input_test_dragging(w32->input_ctx, x, y)) + { + // Window dragging hack + ReleaseCapture(); + SendMessage(w32->window, WM_NCLBUTTONDOWN, HTCAPTION, 0); + mp_input_put_key(w32->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_UP); + + // Indicate the message was handled, so DefWindowProc won't be called + return true; + } + + SetCapture(w32->window); + return false; +} + +static void handle_mouse_up(struct vo_w32_state *w32, int btn) +{ + btn |= mod_state(w32); + mp_input_put_key(w32->input_ctx, btn | MP_KEY_STATE_UP); + + ReleaseCapture(); +} + +static void handle_mouse_wheel(struct vo_w32_state *w32, bool horiz, int val) +{ + int code; + if (horiz) + code = val > 0 ? MP_WHEEL_RIGHT : MP_WHEEL_LEFT; + else + code = val > 0 ? MP_WHEEL_UP : MP_WHEEL_DOWN; + mp_input_put_wheel(w32->input_ctx, code | mod_state(w32), abs(val) / 120.); +} + +static void signal_events(struct vo_w32_state *w32, int events) +{ + atomic_fetch_or(&w32->event_flags, events); + vo_wakeup(w32->vo); +} + +static void wakeup_gui_thread(void *ctx) +{ + struct vo_w32_state *w32 = ctx; + // Wake up the window procedure (which processes the dispatch queue) + if (GetWindowThreadProcessId(w32->window, NULL) == GetCurrentThreadId()) { + PostMessageW(w32->window, WM_NULL, 0, 0); + } else { + // Use a sent message when cross-thread, since the queue of sent + // messages is processed in some cases when posted messages are blocked + SendNotifyMessageW(w32->window, WM_NULL, 0, 0); + } +} + +static double get_refresh_rate_from_gdi(const wchar_t *device) +{ + DEVMODEW dm = { .dmSize = sizeof dm }; + if (!EnumDisplaySettingsW(device, ENUM_CURRENT_SETTINGS, &dm)) + return 0.0; + + // May return 0 or 1 which "represent the display hardware's default refresh rate" + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd183565%28v=vs.85%29.aspx + // mpv validates this value with a threshold of 1, so don't return exactly 1 + if (dm.dmDisplayFrequency == 1) + return 0.0; + + // dm.dmDisplayFrequency is an integer which is rounded down, so it's + // highly likely that 23 represents 24/1.001, 59 represents 60/1.001, etc. + // A caller can always reproduce the original value by using floor. + double rv = dm.dmDisplayFrequency; + switch (dm.dmDisplayFrequency) { + case 23: + case 29: + case 47: + case 59: + case 71: + case 89: + case 95: + case 119: + case 143: + case 164: + case 239: + case 359: + case 479: + rv = (rv + 1) / 1.001; + } + + return rv; +} + +static char *get_color_profile(void *ctx, const wchar_t *device) +{ + char *name = NULL; + + HDC ic = CreateICW(device, NULL, NULL, NULL); + if (!ic) + goto done; + wchar_t wname[MAX_PATH + 1]; + if (!GetICMProfileW(ic, &(DWORD){ MAX_PATH }, wname)) + goto done; + + name = mp_to_utf8(ctx, wname); +done: + if (ic) + DeleteDC(ic); + return name; +} + +static void update_dpi(struct vo_w32_state *w32) +{ + UINT dpiX, dpiY; + HDC hdc = NULL; + int dpi = 0; + + if (w32->api.pGetDpiForMonitor && w32->api.pGetDpiForMonitor(w32->monitor, + MDT_EFFECTIVE_DPI, &dpiX, &dpiY) == S_OK) { + dpi = (int)dpiX; + MP_VERBOSE(w32, "DPI detected from the new API: %d\n", dpi); + } else if ((hdc = GetDC(NULL))) { + dpi = GetDeviceCaps(hdc, LOGPIXELSX); + ReleaseDC(NULL, hdc); + MP_VERBOSE(w32, "DPI detected from the old API: %d\n", dpi); + } + + if (dpi <= 0) { + dpi = 96; + MP_VERBOSE(w32, "Couldn't determine DPI, falling back to %d\n", dpi); + } + + w32->dpi = dpi; + w32->dpi_scale = w32->opts->hidpi_window_scale ? w32->dpi / 96.0 : 1.0; + signal_events(w32, VO_EVENT_DPI); +} + +static void update_display_info(struct vo_w32_state *w32) +{ + HMONITOR monitor = MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY); + if (w32->monitor == monitor) + return; + w32->monitor = monitor; + + update_dpi(w32); + + MONITORINFOEXW mi = { .cbSize = sizeof mi }; + GetMonitorInfoW(monitor, (MONITORINFO*)&mi); + + // Try to get the monitor refresh rate. + double freq = 0.0; + + if (freq == 0.0) + freq = mp_w32_displayconfig_get_refresh_rate(mi.szDevice); + if (freq == 0.0) + freq = get_refresh_rate_from_gdi(mi.szDevice); + + if (freq != w32->display_fps) { + MP_VERBOSE(w32, "display-fps: %f\n", freq); + if (freq == 0.0) + MP_WARN(w32, "Couldn't determine monitor refresh rate\n"); + w32->display_fps = freq; + signal_events(w32, VO_EVENT_WIN_STATE); + } + + char *color_profile = get_color_profile(w32, mi.szDevice); + if ((color_profile == NULL) != (w32->color_profile == NULL) || + (color_profile && strcmp(color_profile, w32->color_profile))) + { + if (color_profile) + MP_VERBOSE(w32, "color-profile: %s\n", color_profile); + talloc_free(w32->color_profile); + w32->color_profile = color_profile; + color_profile = NULL; + signal_events(w32, VO_EVENT_ICC_PROFILE_CHANGED); + } + + talloc_free(color_profile); +} + +static void force_update_display_info(struct vo_w32_state *w32) +{ + w32->monitor = 0; + update_display_info(w32); +} + +static void update_playback_state(struct vo_w32_state *w32) +{ + struct voctrl_playback_state *pstate = &w32->current_pstate; + + if (!w32->taskbar_list3 || !w32->tbtnCreated) + return; + + if (!pstate->playing || !pstate->taskbar_progress) { + ITaskbarList3_SetProgressState(w32->taskbar_list3, w32->window, + TBPF_NOPROGRESS); + return; + } + + ITaskbarList3_SetProgressValue(w32->taskbar_list3, w32->window, + pstate->percent_pos, 100); + ITaskbarList3_SetProgressState(w32->taskbar_list3, w32->window, + pstate->paused ? TBPF_PAUSED : + TBPF_NORMAL); +} + +struct get_monitor_data { + int i; + int target; + HMONITOR mon; +}; + +static BOOL CALLBACK get_monitor_proc(HMONITOR mon, HDC dc, LPRECT r, LPARAM p) +{ + struct get_monitor_data *data = (struct get_monitor_data*)p; + + if (data->i == data->target) { + data->mon = mon; + return FALSE; + } + data->i++; + return TRUE; +} + +static HMONITOR get_monitor(int id) +{ + struct get_monitor_data data = { .target = id }; + EnumDisplayMonitors(NULL, NULL, get_monitor_proc, (LPARAM)&data); + return data.mon; +} + +static HMONITOR get_default_monitor(struct vo_w32_state *w32) +{ + const int id = w32->current_fs ? w32->opts->fsscreen_id : + w32->opts->screen_id; + + // Handle --fs-screen=<all|default> and --screen=default + if (id < 0) { + if (w32->win_force_pos && !w32->current_fs) { + // Get window from forced position + return MonitorFromRect(&w32->windowrc, MONITOR_DEFAULTTOPRIMARY); + } else { + // Let compositor decide + return MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY); + } + } + + HMONITOR mon = get_monitor(id); + if (mon) + return mon; + MP_VERBOSE(w32, "Screen %d does not exist, falling back to primary\n", id); + return MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY); +} + +static MONITORINFO get_monitor_info(struct vo_w32_state *w32) +{ + HMONITOR mon; + if (IsWindowVisible(w32->window) && !w32->current_fs) { + mon = MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY); + } else { + // The window is not visible during initialization, so get the + // monitor by --screen or --fs-screen id, or fallback to primary. + mon = get_default_monitor(w32); + } + MONITORINFO mi = { .cbSize = sizeof(mi) }; + GetMonitorInfoW(mon, &mi); + return mi; +} + +static RECT get_screen_area(struct vo_w32_state *w32) +{ + // Handle --fs-screen=all + if (w32->current_fs && w32->opts->fsscreen_id == -2) { + const int x = GetSystemMetrics(SM_XVIRTUALSCREEN); + const int y = GetSystemMetrics(SM_YVIRTUALSCREEN); + return (RECT) { x, y, x + GetSystemMetrics(SM_CXVIRTUALSCREEN), + y + GetSystemMetrics(SM_CYVIRTUALSCREEN) }; + } + return get_monitor_info(w32).rcMonitor; +} + +static RECT get_working_area(struct vo_w32_state *w32) +{ + return w32->current_fs ? get_screen_area(w32) : + get_monitor_info(w32).rcWork; +} + +// Adjust working area boundaries to compensate for invisible borders. +static void adjust_working_area_for_extended_frame(RECT *wa_rect, RECT *wnd_rect, HWND wnd) +{ + RECT frame = {0}; + + if (DwmGetWindowAttribute(wnd, DWMWA_EXTENDED_FRAME_BOUNDS, + &frame, sizeof(RECT)) == S_OK) { + wa_rect->left -= frame.left - wnd_rect->left; + wa_rect->top -= frame.top - wnd_rect->top; + wa_rect->right += wnd_rect->right - frame.right; + wa_rect->bottom += wnd_rect->bottom - frame.bottom; + } +} + +static bool snap_to_screen_edges(struct vo_w32_state *w32, RECT *rc) +{ + if (w32->parent || w32->current_fs || IsMaximized(w32->window)) + return false; + + if (!w32->opts->snap_window) { + w32->snapped = 0; + return false; + } + + RECT rect; + POINT cursor; + if (!GetWindowRect(w32->window, &rect) || !GetCursorPos(&cursor)) + return false; + // Check if window is going to be aero-snapped + if (rect_w(*rc) != rect_w(rect) || rect_h(*rc) != rect_h(rect)) + return false; + + // Check if window has already been aero-snapped + WINDOWPLACEMENT wp = {0}; + wp.length = sizeof(wp); + if (!GetWindowPlacement(w32->window, &wp)) + return false; + RECT wr = wp.rcNormalPosition; + if (rect_w(*rc) != rect_w(wr) || rect_h(*rc) != rect_h(wr)) + return false; + + // Get the work area to let the window snap to taskbar + wr = get_working_area(w32); + + adjust_working_area_for_extended_frame(&wr, &rect, w32->window); + + // Let the window to unsnap by changing its position, + // otherwise it will stick to the screen edges forever + rect = *rc; + if (w32->snapped) { + OffsetRect(&rect, cursor.x - rect.left - w32->snap_dx, + cursor.y - rect.top - w32->snap_dy); + } + + int threshold = (w32->dpi * 16) / 96; + bool was_snapped = !!w32->snapped; + w32->snapped = 0; + // Adjust X position + // snapped_left & snapped_right are mutually exclusive + if (abs(rect.left - wr.left) < threshold) { + w32->snapped_left = 1; + OffsetRect(&rect, wr.left - rect.left, 0); + } else if (abs(rect.right - wr.right) < threshold) { + w32->snapped_right = 1; + OffsetRect(&rect, wr.right - rect.right, 0); + } + // Adjust Y position + // snapped_top & snapped_bottom are mutually exclusive + if (abs(rect.top - wr.top) < threshold) { + w32->snapped_top = 1; + OffsetRect(&rect, 0, wr.top - rect.top); + } else if (abs(rect.bottom - wr.bottom) < threshold) { + w32->snapped_bottom = 1; + OffsetRect(&rect, 0, wr.bottom - rect.bottom); + } + + if (!was_snapped && w32->snapped != 0) { + w32->snap_dx = cursor.x - rc->left; + w32->snap_dy = cursor.y - rc->top; + } + + *rc = rect; + return true; +} + +static DWORD update_style(struct vo_w32_state *w32, DWORD style) +{ + const DWORD NO_FRAME = WS_OVERLAPPED | WS_MINIMIZEBOX | WS_THICKFRAME; + const DWORD FRAME = WS_OVERLAPPEDWINDOW; + const DWORD FULLSCREEN = NO_FRAME & ~WS_THICKFRAME; + style &= ~(NO_FRAME | FRAME | FULLSCREEN); + style |= WS_SYSMENU; + if (w32->current_fs) { + style |= FULLSCREEN; + } else { + style |= w32->opts->border ? FRAME : NO_FRAME; + } + return style; +} + +static LONG get_title_bar_height(struct vo_w32_state *w32) +{ + RECT rc = {0}; + adjust_window_rect(w32, w32->window, &rc); + return -rc.top; +} + +static void update_window_style(struct vo_w32_state *w32) +{ + if (w32->parent) + return; + + // SetWindowLongPtr can trigger a WM_SIZE event, so window rect + // has to be saved now and restored after setting the new style. + const RECT wr = w32->windowrc; + const DWORD style = GetWindowLongPtrW(w32->window, GWL_STYLE); + SetWindowLongPtrW(w32->window, GWL_STYLE, update_style(w32, style)); + w32->windowrc = wr; +} + +// Resize window rect to width = w and height = h. If window is snapped, +// don't let it detach from snapped borders. Otherwise resize around the center. +static void resize_and_move_rect(struct vo_w32_state *w32, RECT *rc, int w, int h) +{ + int x, y; + + if (w32->snapped_left) + x = rc->left; + else if (w32->snapped_right) + x = rc->right - w; + else + x = rc->left + rect_w(*rc) / 2 - w / 2; + + if (w32->snapped_top) + y = rc->top; + else if (w32->snapped_bottom) + y = rc->bottom - h; + else + y = rc->top + rect_h(*rc) / 2 - h / 2; + + SetRect(rc, x, y, x + w, y + h); +} + +// If rc is wider/taller than n_w/n_h, shrink rc size while keeping the center. +// returns true if the rectangle was modified. +static bool fit_rect_size(struct vo_w32_state *w32, RECT *rc, long n_w, long n_h) +{ + // nothing to do if we already fit. + int o_w = rect_w(*rc), o_h = rect_h(*rc); + if (o_w <= n_w && o_h <= n_h) + return false; + + // Apply letterboxing + const float o_asp = o_w / (float)MPMAX(o_h, 1); + const float n_asp = n_w / (float)MPMAX(n_h, 1); + if (o_asp > n_asp) { + n_h = n_w / o_asp; + } else { + n_w = n_h * o_asp; + } + + resize_and_move_rect(w32, rc, n_w, n_h); + + return true; +} + +// If the window is bigger than the desktop, shrink to fit with same center. +// Also, if the top edge is above the working area, move down to align. +static void fit_window_on_screen(struct vo_w32_state *w32) +{ + RECT screen = get_working_area(w32); + if (w32->opts->border) + subtract_window_borders(w32, w32->window, &screen); + + RECT window_rect; + if (GetWindowRect(w32->window, &window_rect)) + adjust_working_area_for_extended_frame(&screen, &window_rect, w32->window); + + bool adjusted = fit_rect_size(w32, &w32->windowrc, rect_w(screen), rect_h(screen)); + + if (w32->windowrc.top < screen.top) { + // if the top-edge of client area is above the target area (mainly + // because the client-area is centered but the title bar is taller + // than the bottom border), then move it down to align the edges. + // Windows itself applies the same constraint during manual move. + w32->windowrc.bottom += screen.top - w32->windowrc.top; + w32->windowrc.top = screen.top; + adjusted = true; + } + + if (adjusted) { + MP_VERBOSE(w32, "adjusted window bounds: %d:%d:%d:%d\n", + (int)w32->windowrc.left, (int)w32->windowrc.top, + (int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc)); + } +} + +// Calculate new fullscreen state and change window size and position. +static void update_fullscreen_state(struct vo_w32_state *w32) +{ + if (w32->parent) + return; + + bool new_fs = w32->opts->fullscreen; + if (w32->toggle_fs) { + new_fs = !w32->current_fs; + w32->toggle_fs = false; + } + + bool toggle_fs = w32->current_fs != new_fs; + w32->opts->fullscreen = w32->current_fs = new_fs; + m_config_cache_write_opt(w32->opts_cache, + &w32->opts->fullscreen); + + if (toggle_fs) { + if (w32->current_fs) { + // Save window rect when switching to fullscreen. + w32->prev_windowrc = w32->windowrc; + MP_VERBOSE(w32, "save window bounds: %d:%d:%d:%d\n", + (int)w32->windowrc.left, (int)w32->windowrc.top, + (int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc)); + } else { + // Restore window rect when switching from fullscreen. + w32->windowrc = w32->prev_windowrc; + } + } + + if (w32->current_fs) + w32->windowrc = get_screen_area(w32); + + MP_VERBOSE(w32, "reset window bounds: %d:%d:%d:%d\n", + (int)w32->windowrc.left, (int)w32->windowrc.top, + (int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc)); +} + +static void update_minimized_state(struct vo_w32_state *w32) +{ + if (w32->parent) + return; + + if (!!IsMinimized(w32->window) != w32->opts->window_minimized) { + if (w32->opts->window_minimized) { + ShowWindow(w32->window, SW_SHOWMINNOACTIVE); + } else { + ShowWindow(w32->window, SW_RESTORE); + } + } +} + +static void update_maximized_state(struct vo_w32_state *w32) +{ + if (w32->parent) + return; + + // Don't change the maximized state in fullscreen for now. In future, this + // should be made to apply the maximized state on leaving fullscreen. + if (w32->current_fs) + return; + + WINDOWPLACEMENT wp = { .length = sizeof wp }; + GetWindowPlacement(w32->window, &wp); + + if (wp.showCmd == SW_SHOWMINIMIZED) { + // When the window is minimized, setting this property just changes + // whether it will be maximized when it's restored + if (w32->opts->window_maximized) { + wp.flags |= WPF_RESTORETOMAXIMIZED; + } else { + wp.flags &= ~WPF_RESTORETOMAXIMIZED; + } + SetWindowPlacement(w32->window, &wp); + } else if ((wp.showCmd == SW_SHOWMAXIMIZED) != w32->opts->window_maximized) { + if (w32->opts->window_maximized) { + ShowWindow(w32->window, SW_SHOWMAXIMIZED); + } else { + ShowWindow(w32->window, SW_SHOWNOACTIVATE); + } + } +} + +static bool is_visible(HWND window) +{ + // Unlike IsWindowVisible, this doesn't check the window's parents + return GetWindowLongPtrW(window, GWL_STYLE) & WS_VISIBLE; +} + +//Set the mpv window's affinity. +//This will affect how it's displayed on the desktop and in system-level operations like taking screenshots. +static void update_affinity(struct vo_w32_state *w32) +{ + if (!w32 || w32->parent) { + return; + } + SetWindowDisplayAffinity(w32->window, w32->opts->window_affinity); +} + +static void update_window_state(struct vo_w32_state *w32) +{ + if (w32->parent) + return; + + RECT wr = w32->windowrc; + add_window_borders(w32, w32->window, &wr); + + SetWindowPos(w32->window, w32->opts->ontop ? HWND_TOPMOST : HWND_NOTOPMOST, + wr.left, wr.top, rect_w(wr), rect_h(wr), + SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOOWNERZORDER); + + // Show the window if it's not yet visible + if (!is_visible(w32->window)) { + if (w32->opts->window_minimized) { + ShowWindow(w32->window, SW_SHOWMINNOACTIVE); + update_maximized_state(w32); // Set the WPF_RESTORETOMAXIMIZED flag + } else if (w32->opts->window_maximized) { + ShowWindow(w32->window, SW_SHOWMAXIMIZED); + } else { + ShowWindow(w32->window, SW_SHOW); + } + } + + // Notify the taskbar about the fullscreen state only after the window + // is visible, to make sure the taskbar item has already been created + if (w32->taskbar_list) { + ITaskbarList2_MarkFullscreenWindow(w32->taskbar_list, + w32->window, w32->current_fs); + } + + // Update snapping status if needed + if (w32->opts->snap_window && !w32->parent && + !w32->current_fs && !IsMaximized(w32->window)) { + RECT wa = get_working_area(w32); + + adjust_working_area_for_extended_frame(&wa, &wr, w32->window); + + // snapped_left & snapped_right are mutually exclusive + if (wa.left == wr.left && wa.right == wr.right) { + // Leave as is. + } else if (wa.left == wr.left) { + w32->snapped_left = 1; + w32->snapped_right = 0; + } else if (wa.right == wr.right) { + w32->snapped_right = 1; + w32->snapped_left = 0; + } else { + w32->snapped_left = w32->snapped_right = 0; + } + + // snapped_top & snapped_bottom are mutually exclusive + if (wa.top == wr.top && wa.bottom == wr.bottom) { + // Leave as is. + } else if (wa.top == wr.top) { + w32->snapped_top = 1; + w32->snapped_bottom = 0; + } else if (wa.bottom == wr.bottom) { + w32->snapped_bottom = 1; + w32->snapped_top = 0; + } else { + w32->snapped_top = w32->snapped_bottom = 0; + } + } + + signal_events(w32, VO_EVENT_RESIZE); +} + +static void update_corners_pref(const struct vo_w32_state *w32) { + if (w32->parent) + return; + + int pref = w32->current_fs ? 0 : w32->opts->window_corners; + DwmSetWindowAttribute(w32->window, DWMWA_WINDOW_CORNER_PREFERENCE, + &pref, sizeof(pref)); +} + +static void reinit_window_state(struct vo_w32_state *w32) +{ + if (w32->parent) + return; + + // The order matters: fs state should be updated prior to changing styles + update_fullscreen_state(w32); + update_corners_pref(w32); + update_window_style(w32); + + // fit_on_screen is applied at most once when/if applicable (normal win). + if (w32->fit_on_screen && !w32->current_fs && !IsMaximized(w32->window)) { + fit_window_on_screen(w32); + w32->fit_on_screen = false; + } + + // Show and activate the window after all window state parameters were set + update_window_state(w32); +} + +// Follow Windows settings and update dark mode state +// Microsoft documented how to enable dark mode for title bar: +// https://learn.microsoft.com/windows/apps/desktop/modernize/apply-windows-themes +// https://learn.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +// Documentation says to set the DWMWA_USE_IMMERSIVE_DARK_MODE attribute to +// TRUE to honor dark mode for the window, FALSE to always use light mode. While +// in fact setting it to TRUE causes dark mode to be always enabled, regardless +// of the settings. Since it is quite unlikely that it will be fixed, just use +// UxTheme API to check if dark mode should be applied and while at it enable it +// fully. Ideally this function should only call the DwmSetWindowAttribute(), +// but it just doesn't work as documented. +static void update_dark_mode(const struct vo_w32_state *w32) +{ + if (w32->api.pSetPreferredAppMode) + w32->api.pSetPreferredAppMode(1); // allow dark mode + + HIGHCONTRAST hc = {sizeof(hc)}; + SystemParametersInfo(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0); + bool high_contrast = hc.dwFlags & HCF_HIGHCONTRASTON; + + // if pShouldAppsUseDarkMode is not available, just assume it to be true + const BOOL use_dark_mode = !high_contrast && (!w32->api.pShouldAppsUseDarkMode || + w32->api.pShouldAppsUseDarkMode()); + + SetWindowTheme(w32->window, use_dark_mode ? L"DarkMode_Explorer" : L"", NULL); + + DwmSetWindowAttribute(w32->window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &use_dark_mode, sizeof(use_dark_mode)); +} + +static void update_backdrop(const struct vo_w32_state *w32) +{ + if (w32->parent) + return; + + int backdropType = w32->opts->backdrop_type; + DwmSetWindowAttribute(w32->window, DWMWA_SYSTEMBACKDROP_TYPE, + &backdropType, sizeof(backdropType)); +} + +static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, + LPARAM lParam) +{ + struct vo_w32_state *w32 = (void*)GetWindowLongPtrW(hWnd, GWLP_USERDATA); + if (!w32) { + // WM_NCCREATE is supposed to be the first message that a window + // receives. It allows struct vo_w32_state to be passed from + // CreateWindow's lpParam to the window procedure. However, as a + // longstanding Windows bug, overlapped top-level windows will get a + // WM_GETMINMAXINFO before WM_NCCREATE. This can be ignored. + if (message != WM_NCCREATE) + return DefWindowProcW(hWnd, message, wParam, lParam); + + CREATESTRUCTW *cs = (CREATESTRUCTW *)lParam; + w32 = cs->lpCreateParams; + w32->window = hWnd; + SetWindowLongPtrW(hWnd, GWLP_USERDATA, (LONG_PTR)w32); + } + + // The dispatch queue should be processed as soon as possible to prevent + // playback glitches, since it is likely blocking the VO thread + if (!w32->in_dispatch) { + w32->in_dispatch = true; + mp_dispatch_queue_process(w32->dispatch, 0); + w32->in_dispatch = false; + } + + switch (message) { + case WM_ERASEBKGND: + if (w32->cleared || !w32->opts->border || w32->current_fs) + return TRUE; + break; + case WM_PAINT: + w32->cleared = true; + signal_events(w32, VO_EVENT_EXPOSE); + break; + case WM_MOVE: { + w32->moving = false; + const int x = GET_X_LPARAM(lParam), y = GET_Y_LPARAM(lParam); + OffsetRect(&w32->windowrc, x - w32->windowrc.left, + y - w32->windowrc.top); + + // Window may intersect with new monitors (see VOCTRL_GET_DISPLAY_NAMES) + signal_events(w32, VO_EVENT_WIN_STATE); + + update_display_info(w32); // if we moved between monitors + break; + } + case WM_MOVING: { + w32->moving = true; + RECT *rc = (RECT*)lParam; + if (snap_to_screen_edges(w32, rc)) + return TRUE; + break; + } + case WM_ENTERSIZEMOVE: + w32->moving = true; + if (w32->snapped != 0) { + // Save the cursor offset from the window borders, + // so the player window can be unsnapped later + RECT rc; + POINT cursor; + if (GetWindowRect(w32->window, &rc) && GetCursorPos(&cursor)) { + w32->snap_dx = cursor.x - rc.left; + w32->snap_dy = cursor.y - rc.top; + } + } + break; + case WM_EXITSIZEMOVE: + w32->moving = false; + break; + case WM_SIZE: { + const int w = LOWORD(lParam), h = HIWORD(lParam); + if (w > 0 && h > 0) { + w32->windowrc.right = w32->windowrc.left + w; + w32->windowrc.bottom = w32->windowrc.top + h; + signal_events(w32, VO_EVENT_RESIZE); + MP_VERBOSE(w32, "resize window: %d:%d\n", w, h); + } + + // Window may have been minimized, maximized or restored + if (is_visible(w32->window)) { + WINDOWPLACEMENT wp = { .length = sizeof wp }; + GetWindowPlacement(w32->window, &wp); + + bool is_minimized = wp.showCmd == SW_SHOWMINIMIZED; + if (w32->opts->window_minimized != is_minimized) { + w32->opts->window_minimized = is_minimized; + m_config_cache_write_opt(w32->opts_cache, + &w32->opts->window_minimized); + } + + bool is_maximized = wp.showCmd == SW_SHOWMAXIMIZED || + (wp.showCmd == SW_SHOWMINIMIZED && + (wp.flags & WPF_RESTORETOMAXIMIZED)); + if (w32->opts->window_maximized != is_maximized) { + w32->opts->window_maximized = is_maximized; + m_config_cache_write_opt(w32->opts_cache, + &w32->opts->window_maximized); + } + } + + signal_events(w32, VO_EVENT_WIN_STATE); + + update_display_info(w32); + break; + } + case WM_SIZING: + if (w32->opts->keepaspect && w32->opts->keepaspect_window && + !w32->current_fs && !w32->parent) + { + RECT *rc = (RECT*)lParam; + // get client area of the windows if it had the rect rc + // (subtracting the window borders) + RECT r = *rc; + subtract_window_borders(w32, w32->window, &r); + int c_w = rect_w(r), c_h = rect_h(r); + float aspect = w32->o_dwidth / (float) MPMAX(w32->o_dheight, 1); + int d_w = c_h * aspect - c_w; + int d_h = c_w / aspect - c_h; + int d_corners[4] = { d_w, d_h, -d_w, -d_h }; + int corners[4] = { rc->left, rc->top, rc->right, rc->bottom }; + int corner = get_resize_border(w32, wParam); + if (corner >= 0) + corners[corner] -= d_corners[corner]; + *rc = (RECT) { corners[0], corners[1], corners[2], corners[3] }; + return TRUE; + } + break; + case WM_DPICHANGED: + update_display_info(w32); + + RECT *rc = (RECT*)lParam; + w32->windowrc = *rc; + subtract_window_borders(w32, w32->window, &w32->windowrc); + update_window_state(w32); + break; + case WM_CLOSE: + // Don't destroy the window yet to not lose wakeup events. + mp_input_put_key(w32->input_ctx, MP_KEY_CLOSE_WIN); + return 0; + case WM_NCDESTROY: // Sometimes only WM_NCDESTROY is received in --wid mode + case WM_DESTROY: + if (w32->destroyed) + break; + // If terminate is not set, something else destroyed the window. This + // can also happen in --wid mode when the parent window is destroyed. + if (!w32->terminate) + mp_input_put_key(w32->input_ctx, MP_KEY_CLOSE_WIN); + RevokeDragDrop(w32->window); + w32->destroyed = true; + w32->window = NULL; + PostQuitMessage(0); + break; + case WM_SYSCOMMAND: + switch (wParam & 0xFFF0) { + case SC_SCREENSAVE: + case SC_MONITORPOWER: + if (w32->disable_screensaver) { + MP_VERBOSE(w32, "killing screensaver\n"); + return 0; + } + break; + case SC_RESTORE: + if (IsMaximized(w32->window) && w32->current_fs) { + w32->toggle_fs = true; + reinit_window_state(w32); + + return 0; + } + break; + } + break; + case WM_NCACTIVATE: + // Cosmetic to remove blinking window border when initializing window + if (!w32->opts->border) + lParam = -1; + break; + case WM_NCHITTEST: + // Provide sizing handles for borderless windows + if ((!w32->opts->border || !w32->opts->title_bar) && !w32->current_fs) { + return borderless_nchittest(w32, GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam)); + } + break; + case WM_APPCOMMAND: + if (handle_appcommand(w32, GET_APPCOMMAND_LPARAM(lParam))) + return TRUE; + break; + case WM_SYSKEYDOWN: + // Open the window menu on Alt+Space. Normally DefWindowProc opens the + // window menu in response to WM_SYSCHAR, but since mpv translates its + // own keyboard input, WM_SYSCHAR isn't generated, so the window menu + // must be opened manually. + if (wParam == VK_SPACE) { + SendMessage(w32->window, WM_SYSCOMMAND, SC_KEYMENU, ' '); + return 0; + } + + handle_key_down(w32, wParam, HIWORD(lParam)); + if (wParam == VK_F10) + return 0; + break; + case WM_KEYDOWN: + handle_key_down(w32, wParam, HIWORD(lParam)); + break; + case WM_SYSKEYUP: + case WM_KEYUP: + handle_key_up(w32, wParam, HIWORD(lParam)); + if (wParam == VK_F10) + return 0; + break; + case WM_CHAR: + case WM_SYSCHAR: + if (handle_char(w32, wParam)) + return 0; + break; + case WM_KILLFOCUS: + mp_input_put_key(w32->input_ctx, MP_INPUT_RELEASE_ALL); + w32->focused = false; + signal_events(w32, VO_EVENT_FOCUS); + return 0; + case WM_SETFOCUS: + w32->focused = true; + signal_events(w32, VO_EVENT_FOCUS); + return 0; + case WM_SETCURSOR: + // The cursor should only be hidden if the mouse is in the client area + // and if the window isn't in menu mode (HIWORD(lParam) is non-zero) + w32->can_set_cursor = LOWORD(lParam) == HTCLIENT && HIWORD(lParam); + if (w32->can_set_cursor && !w32->cursor_visible) { + SetCursor(NULL); + return TRUE; + } + break; + case WM_MOUSELEAVE: + w32->tracking = FALSE; + mp_input_put_key(w32->input_ctx, MP_KEY_MOUSE_LEAVE); + break; + case WM_MOUSEMOVE: { + if (!w32->tracking) { + w32->tracking = TrackMouseEvent(&w32->trackEvent); + mp_input_put_key(w32->input_ctx, MP_KEY_MOUSE_ENTER); + } + // Windows can send spurious mouse events, which would make the mpv + // core unhide the mouse cursor on completely unrelated events. See: + // https://blogs.msdn.com/b/oldnewthing/archive/2003/10/01/55108.aspx + int x = GET_X_LPARAM(lParam); + int y = GET_Y_LPARAM(lParam); + if (x != w32->mouse_x || y != w32->mouse_y) { + w32->mouse_x = x; + w32->mouse_y = y; + mp_input_set_mouse_pos(w32->input_ctx, x, y); + } + break; + } + case WM_LBUTTONDOWN: + if (handle_mouse_down(w32, MP_MBTN_LEFT, GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam))) + return 0; + break; + case WM_LBUTTONUP: + handle_mouse_up(w32, MP_MBTN_LEFT); + break; + case WM_MBUTTONDOWN: + handle_mouse_down(w32, MP_MBTN_MID, GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam)); + break; + case WM_MBUTTONUP: + handle_mouse_up(w32, MP_MBTN_MID); + break; + case WM_RBUTTONDOWN: + handle_mouse_down(w32, MP_MBTN_RIGHT, GET_X_LPARAM(lParam), + GET_Y_LPARAM(lParam)); + break; + case WM_RBUTTONUP: + handle_mouse_up(w32, MP_MBTN_RIGHT); + break; + case WM_MOUSEWHEEL: + handle_mouse_wheel(w32, false, GET_WHEEL_DELTA_WPARAM(wParam)); + return 0; + case WM_MOUSEHWHEEL: + handle_mouse_wheel(w32, true, GET_WHEEL_DELTA_WPARAM(wParam)); + // Some buggy mouse drivers (SetPoint) stop delivering WM_MOUSEHWHEEL + // events when the message loop doesn't return TRUE (even on Windows 7) + return TRUE; + case WM_XBUTTONDOWN: + handle_mouse_down(w32, + HIWORD(wParam) == 1 ? MP_MBTN_BACK : MP_MBTN_FORWARD, + GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + break; + case WM_XBUTTONUP: + handle_mouse_up(w32, + HIWORD(wParam) == 1 ? MP_MBTN_BACK : MP_MBTN_FORWARD); + break; + case WM_DISPLAYCHANGE: + force_update_display_info(w32); + break; + case WM_SETTINGCHANGE: + update_dark_mode(w32); + break; + case WM_NCCALCSIZE: + if (!w32->opts->border) + return 0; + // Apparently removing WS_CAPTION disables some window animation, instead + // just reduce non-client size to remove title bar. + if (wParam && lParam && w32->opts->border && !w32->opts->title_bar && + !w32->current_fs && !w32->parent) + { + ((LPNCCALCSIZE_PARAMS) lParam)->rgrc[0].top -= get_title_bar_height(w32); + } + break; + } + + if (message == w32->tbtnCreatedMsg) { + w32->tbtnCreated = true; + update_playback_state(w32); + return 0; + } + + return DefWindowProcW(hWnd, message, wParam, lParam); +} + +static mp_once window_class_init_once = MP_STATIC_ONCE_INITIALIZER; +static ATOM window_class; +static void register_window_class(void) +{ + window_class = RegisterClassExW(&(WNDCLASSEXW) { + .cbSize = sizeof(WNDCLASSEXW), + .style = CS_HREDRAW | CS_VREDRAW, + .lpfnWndProc = WndProc, + .hInstance = HINST_THISCOMPONENT, + .hIcon = LoadIconW(HINST_THISCOMPONENT, L"IDI_ICON1"), + .hCursor = LoadCursor(NULL, IDC_ARROW), + .hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH), + .lpszClassName = L"mpv", + }); +} + +static ATOM get_window_class(void) +{ + mp_exec_once(&window_class_init_once, register_window_class); + return window_class; +} + +static void resize_child_win(HWND parent) +{ + // Check if an mpv window is a child of this window. This will not + // necessarily be the case because the hook functions will run for all + // windows on the parent window's thread. + ATOM cls = get_window_class(); + HWND child = FindWindowExW(parent, NULL, (LPWSTR)MAKEINTATOM(cls), NULL); + if (!child) + return; + // Make sure the window was created by this instance + if (GetWindowLongPtrW(child, GWLP_HINSTANCE) != (LONG_PTR)HINST_THISCOMPONENT) + return; + + // Resize the mpv window to match its parent window's size + RECT rm, rp; + if (!GetClientRect(child, &rm)) + return; + if (!GetClientRect(parent, &rp)) + return; + if (EqualRect(&rm, &rp)) + return; + SetWindowPos(child, NULL, 0, 0, rp.right, rp.bottom, SWP_ASYNCWINDOWPOS | + SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOSENDCHANGING); +} + +static LRESULT CALLBACK parent_win_hook(int nCode, WPARAM wParam, LPARAM lParam) +{ + if (nCode != HC_ACTION) + goto done; + CWPSTRUCT *cwp = (CWPSTRUCT*)lParam; + if (cwp->message != WM_WINDOWPOSCHANGED) + goto done; + resize_child_win(cwp->hwnd); +done: + return CallNextHookEx(NULL, nCode, wParam, lParam); +} + +static void CALLBACK parent_evt_hook(HWINEVENTHOOK hWinEventHook, DWORD event, + HWND hwnd, LONG idObject, LONG idChild, DWORD dwEventThread, + DWORD dwmsEventTime) +{ + if (event != EVENT_OBJECT_LOCATIONCHANGE) + return; + if (!hwnd || idObject != OBJID_WINDOW || idChild != CHILDID_SELF) + return; + resize_child_win(hwnd); +} + +static void install_parent_hook(struct vo_w32_state *w32) +{ + DWORD pid; + DWORD tid = GetWindowThreadProcessId(w32->parent, &pid); + + // If the parent lives inside the current process, install a Windows hook + if (pid == GetCurrentProcessId()) { + w32->parent_win_hook = SetWindowsHookExW(WH_CALLWNDPROC, + parent_win_hook, NULL, tid); + } else { + // Otherwise, use a WinEvent hook. These don't seem to be as smooth as + // Windows hooks, but they can be delivered across process boundaries. + w32->parent_evt_hook = SetWinEventHook( + EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, + NULL, parent_evt_hook, pid, tid, WINEVENT_OUTOFCONTEXT); + } +} + +static void remove_parent_hook(struct vo_w32_state *w32) +{ + if (w32->parent_win_hook) + UnhookWindowsHookEx(w32->parent_win_hook); + if (w32->parent_evt_hook) + UnhookWinEvent(w32->parent_evt_hook); +} + +// Dispatch incoming window events and handle them. +// This returns only when the thread is asked to terminate. +static void run_message_loop(struct vo_w32_state *w32) +{ + MSG msg; + while (GetMessageW(&msg, 0, 0, 0) > 0) + DispatchMessageW(&msg); + + // Even if the message loop somehow exits, we still have to respond to + // external requests until termination is requested. + while (!w32->terminate) + mp_dispatch_queue_process(w32->dispatch, 1000); +} + +static void gui_thread_reconfig(void *ptr) +{ + struct vo_w32_state *w32 = ptr; + struct vo *vo = w32->vo; + + RECT r = get_working_area(w32); + // for normal window which is auto-positioned (centered), center the window + // rather than the content (by subtracting the borders from the work area) + if (!w32->current_fs && !IsMaximized(w32->window) && w32->opts->border && + !w32->opts->geometry.xy_valid /* specific position not requested */) + { + subtract_window_borders(w32, w32->window, &r); + } + struct mp_rect screen = { r.left, r.top, r.right, r.bottom }; + struct vo_win_geometry geo; + + RECT monrc = get_monitor_info(w32).rcMonitor; + struct mp_rect mon = { monrc.left, monrc.top, monrc.right, monrc.bottom }; + + if (w32->dpi_scale == 0) + force_update_display_info(w32); + + vo_calc_window_geometry3(vo, &screen, &mon, w32->dpi_scale, &geo); + vo_apply_window_geometry(vo, &geo); + + bool reset_size = (w32->o_dwidth != vo->dwidth || + w32->o_dheight != vo->dheight) && + w32->opts->auto_window_resize; + + w32->o_dwidth = vo->dwidth; + w32->o_dheight = vo->dheight; + + if (!w32->parent && !w32->window_bounds_initialized) { + SetRect(&w32->windowrc, geo.win.x0, geo.win.y0, + geo.win.x0 + vo->dwidth, geo.win.y0 + vo->dheight); + w32->prev_windowrc = w32->windowrc; + w32->window_bounds_initialized = true; + w32->win_force_pos = geo.flags & VO_WIN_FORCE_POS; + w32->fit_on_screen = !w32->win_force_pos; + goto finish; + } + + // The rect which size is going to be modified. + RECT *rc = &w32->windowrc; + + // The desired size always matches the window size in wid mode. + if (!reset_size || w32->parent) { + GetClientRect(w32->window, &r); + // Restore vo_dwidth and vo_dheight, which were reset in vo_config() + vo->dwidth = r.right; + vo->dheight = r.bottom; + } else { + if (w32->current_fs) + rc = &w32->prev_windowrc; + w32->fit_on_screen = true; + } + + resize_and_move_rect(w32, rc, vo->dwidth, vo->dheight); + +finish: + reinit_window_state(w32); +} + +// Resize the window. On the first call, it's also made visible. +void vo_w32_config(struct vo *vo) +{ + struct vo_w32_state *w32 = vo->w32; + mp_dispatch_run(w32->dispatch, gui_thread_reconfig, w32); +} + +static void w32_api_load(struct vo_w32_state *w32) +{ + HMODULE shcore_dll = LoadLibraryW(L"shcore.dll"); + // Available since Win8.1 + w32->api.pGetDpiForMonitor = !shcore_dll ? NULL : + (void *)GetProcAddress(shcore_dll, "GetDpiForMonitor"); + + HMODULE user32_dll = LoadLibraryW(L"user32.dll"); + // Available since Win10 + w32->api.pAdjustWindowRectExForDpi = !user32_dll ? NULL : + (void *)GetProcAddress(user32_dll, "AdjustWindowRectExForDpi"); + + // imm32.dll must be loaded dynamically + // to account for machines without East Asian language support + HMODULE imm32_dll = LoadLibraryW(L"imm32.dll"); + w32->api.pImmDisableIME = !imm32_dll ? NULL : + (void *)GetProcAddress(imm32_dll, "ImmDisableIME"); + + // Dark mode related functions, available since the 1809 Windows 10 update + // Check the Windows build version as on previous versions used ordinals + // may point to unexpected code/data. Alternatively could check uxtheme.dll + // version directly, but it is little bit more boilerplate code, and build + // number is good enough check. + void (WINAPI *pRtlGetNtVersionNumbers)(LPDWORD, LPDWORD, LPDWORD) = + (void *)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetNtVersionNumbers"); + + DWORD major, build; + pRtlGetNtVersionNumbers(&major, NULL, &build); + build &= ~0xF0000000; + + HMODULE uxtheme_dll = (major < 10 || build < 17763) ? NULL : + GetModuleHandle(L"uxtheme.dll"); + w32->api.pShouldAppsUseDarkMode = !uxtheme_dll ? NULL : + (void *)GetProcAddress(uxtheme_dll, MAKEINTRESOURCEA(132)); + w32->api.pSetPreferredAppMode = !uxtheme_dll ? NULL : + (void *)GetProcAddress(uxtheme_dll, MAKEINTRESOURCEA(135)); +} + +static MP_THREAD_VOID gui_thread(void *ptr) +{ + struct vo_w32_state *w32 = ptr; + bool ole_ok = false; + int res = 0; + + mp_thread_set_name("window"); + + w32_api_load(w32); + + // Disables the IME for windows on this thread + if (w32->api.pImmDisableIME) + w32->api.pImmDisableIME(0); + + if (w32->opts->WinID >= 0) + w32->parent = (HWND)(intptr_t)(w32->opts->WinID); + + ATOM cls = get_window_class(); + if (w32->parent) { + RECT r; + GetClientRect(w32->parent, &r); + CreateWindowExW(WS_EX_NOPARENTNOTIFY, (LPWSTR)MAKEINTATOM(cls), L"mpv", + WS_CHILD | WS_VISIBLE, 0, 0, r.right, r.bottom, + w32->parent, 0, HINST_THISCOMPONENT, w32); + + // Install a hook to get notifications when the parent changes size + if (w32->window) + install_parent_hook(w32); + } else { + CreateWindowExW(0, (LPWSTR)MAKEINTATOM(cls), L"mpv", + update_style(w32, 0), CW_USEDEFAULT, SW_HIDE, 100, 100, + 0, 0, HINST_THISCOMPONENT, w32); + } + + if (!w32->window) { + MP_ERR(w32, "unable to create window!\n"); + goto done; + } + + update_dark_mode(w32); + update_corners_pref(w32); + if (w32->opts->window_affinity) + update_affinity(w32); + if (w32->opts->backdrop_type) + update_backdrop(w32); + + if (SUCCEEDED(OleInitialize(NULL))) { + ole_ok = true; + + IDropTarget *dt = mp_w32_droptarget_create(w32->log, w32->opts, w32->input_ctx); + RegisterDragDrop(w32->window, dt); + + // ITaskbarList2 has the MarkFullscreenWindow method, which is used to + // make sure the taskbar is hidden when mpv goes fullscreen + if (SUCCEEDED(CoCreateInstance(&CLSID_TaskbarList, NULL, + CLSCTX_INPROC_SERVER, &IID_ITaskbarList2, + (void**)&w32->taskbar_list))) + { + if (FAILED(ITaskbarList2_HrInit(w32->taskbar_list))) { + ITaskbarList2_Release(w32->taskbar_list); + w32->taskbar_list = NULL; + } + } + + // ITaskbarList3 has methods for status indication on taskbar buttons, + // however that interface is only available on Win7/2008 R2 or newer + if (SUCCEEDED(CoCreateInstance(&CLSID_TaskbarList, NULL, + CLSCTX_INPROC_SERVER, &IID_ITaskbarList3, + (void**)&w32->taskbar_list3))) + { + if (FAILED(ITaskbarList3_HrInit(w32->taskbar_list3))) { + ITaskbarList3_Release(w32->taskbar_list3); + w32->taskbar_list3 = NULL; + } else { + w32->tbtnCreatedMsg = RegisterWindowMessage(L"TaskbarButtonCreated"); + } + } + } else { + MP_ERR(w32, "Failed to initialize OLE/COM\n"); + } + + w32->tracking = FALSE; + w32->trackEvent = (TRACKMOUSEEVENT){ + .cbSize = sizeof(TRACKMOUSEEVENT), + .dwFlags = TME_LEAVE, + .hwndTrack = w32->window, + }; + + if (w32->parent) + EnableWindow(w32->window, 0); + + w32->cursor_visible = true; + w32->moving = false; + w32->snapped = 0; + w32->snap_dx = w32->snap_dy = 0; + + mp_dispatch_set_wakeup_fn(w32->dispatch, wakeup_gui_thread, w32); + + res = 1; +done: + + mp_rendezvous(w32, res); // init barrier + + // This blocks until the GUI thread is to be exited. + if (res) + run_message_loop(w32); + + MP_VERBOSE(w32, "uninit\n"); + + remove_parent_hook(w32); + if (w32->window && !w32->destroyed) + DestroyWindow(w32->window); + if (w32->taskbar_list) + ITaskbarList2_Release(w32->taskbar_list); + if (w32->taskbar_list3) + ITaskbarList3_Release(w32->taskbar_list3); + if (ole_ok) + OleUninitialize(); + SetThreadExecutionState(ES_CONTINUOUS); + MP_THREAD_RETURN(); +} + +bool vo_w32_init(struct vo *vo) +{ + assert(!vo->w32); + + struct vo_w32_state *w32 = talloc_ptrtype(vo, w32); + *w32 = (struct vo_w32_state){ + .log = mp_log_new(w32, vo->log, "win32"), + .vo = vo, + .opts_cache = m_config_cache_alloc(w32, vo->global, &vo_sub_opts), + .input_ctx = vo->input_ctx, + .dispatch = mp_dispatch_create(w32), + }; + w32->opts = w32->opts_cache->opts; + vo->w32 = w32; + + if (mp_thread_create(&w32->thread, gui_thread, w32)) + goto fail; + + if (!mp_rendezvous(w32, 0)) { // init barrier + mp_thread_join(w32->thread); + goto fail; + } + + // While the UI runs in its own thread, the thread in which this function + // runs in will be the renderer thread. Apply magic MMCSS cargo-cult, + // which might stop Windows from throttling clock rate and so on. + if (vo->opts->mmcss_profile[0]) { + wchar_t *profile = mp_from_utf8(NULL, vo->opts->mmcss_profile); + w32->avrt_handle = AvSetMmThreadCharacteristicsW(profile, &(DWORD){0}); + talloc_free(profile); + } + + return true; +fail: + talloc_free(w32); + vo->w32 = NULL; + return false; +} + +struct disp_names_data { + HMONITOR assoc; + int count; + char **names; +}; + +static BOOL CALLBACK disp_names_proc(HMONITOR mon, HDC dc, LPRECT r, LPARAM p) +{ + struct disp_names_data *data = (struct disp_names_data*)p; + + // get_disp_names() adds data->assoc to the list, so skip it here + if (mon == data->assoc) + return TRUE; + + MONITORINFOEXW mi = { .cbSize = sizeof mi }; + if (GetMonitorInfoW(mon, (MONITORINFO*)&mi)) { + MP_TARRAY_APPEND(NULL, data->names, data->count, + mp_to_utf8(NULL, mi.szDevice)); + } + return TRUE; +} + +static char **get_disp_names(struct vo_w32_state *w32) +{ + // Get the client area of the window in screen space + RECT rect = { 0 }; + GetClientRect(w32->window, &rect); + MapWindowPoints(w32->window, NULL, (POINT*)&rect, 2); + + struct disp_names_data data = { .assoc = w32->monitor }; + + // Make sure the monitor that Windows considers to be associated with the + // window is first in the list + MONITORINFOEXW mi = { .cbSize = sizeof mi }; + if (GetMonitorInfoW(data.assoc, (MONITORINFO*)&mi)) { + MP_TARRAY_APPEND(NULL, data.names, data.count, + mp_to_utf8(NULL, mi.szDevice)); + } + + // Get the names of the other monitors that intersect the client rect + EnumDisplayMonitors(NULL, &rect, disp_names_proc, (LPARAM)&data); + MP_TARRAY_APPEND(NULL, data.names, data.count, NULL); + return data.names; +} + +static int gui_thread_control(struct vo_w32_state *w32, int request, void *arg) +{ + switch (request) { + case VOCTRL_VO_OPTS_CHANGED: { + void *changed_option; + + while (m_config_cache_get_next_changed(w32->opts_cache, + &changed_option)) + { + struct mp_vo_opts *vo_opts = w32->opts_cache->opts; + + if (changed_option == &vo_opts->fullscreen) { + reinit_window_state(w32); + } else if (changed_option == &vo_opts->window_affinity) { + update_affinity(w32); + } else if (changed_option == &vo_opts->ontop) { + update_window_state(w32); + } else if (changed_option == &vo_opts->backdrop_type) { + update_backdrop(w32); + } else if (changed_option == &vo_opts->border || + changed_option == &vo_opts->title_bar) + { + update_window_style(w32); + update_window_state(w32); + } else if (changed_option == &vo_opts->window_minimized) { + update_minimized_state(w32); + } else if (changed_option == &vo_opts->window_maximized) { + update_maximized_state(w32); + } else if (changed_option == &vo_opts->window_corners) { + update_corners_pref(w32); + } + } + + return VO_TRUE; + } + case VOCTRL_GET_WINDOW_ID: { + if (!w32->window) + return VO_NOTAVAIL; + *(int64_t *)arg = (intptr_t)w32->window; + return VO_TRUE; + } + case VOCTRL_GET_HIDPI_SCALE: { + *(double *)arg = w32->dpi_scale; + return VO_TRUE; + } + case VOCTRL_GET_UNFS_WINDOW_SIZE: { + int *s = arg; + + if (!w32->window_bounds_initialized) + return VO_FALSE; + + RECT *rc = w32->current_fs ? &w32->prev_windowrc : &w32->windowrc; + s[0] = rect_w(*rc) / w32->dpi_scale; + s[1] = rect_h(*rc) / w32->dpi_scale; + return VO_TRUE; + } + case VOCTRL_SET_UNFS_WINDOW_SIZE: { + int *s = arg; + + if (!w32->window_bounds_initialized) + return VO_FALSE; + + s[0] *= w32->dpi_scale; + s[1] *= w32->dpi_scale; + + RECT *rc = w32->current_fs ? &w32->prev_windowrc : &w32->windowrc; + resize_and_move_rect(w32, rc, s[0], s[1]); + + w32->fit_on_screen = true; + reinit_window_state(w32); + return VO_TRUE; + } + case VOCTRL_SET_CURSOR_VISIBILITY: + w32->cursor_visible = *(bool *)arg; + + if (w32->can_set_cursor && w32->tracking) { + if (w32->cursor_visible) + SetCursor(LoadCursor(NULL, IDC_ARROW)); + else + SetCursor(NULL); + } + return VO_TRUE; + case VOCTRL_KILL_SCREENSAVER: + w32->disable_screensaver = true; + SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | + ES_DISPLAY_REQUIRED); + return VO_TRUE; + case VOCTRL_RESTORE_SCREENSAVER: + w32->disable_screensaver = false; + SetThreadExecutionState(ES_CONTINUOUS); + return VO_TRUE; + case VOCTRL_UPDATE_WINDOW_TITLE: { + wchar_t *title = mp_from_utf8(NULL, (char *)arg); + SetWindowTextW(w32->window, title); + talloc_free(title); + return VO_TRUE; + } + case VOCTRL_UPDATE_PLAYBACK_STATE: { + w32->current_pstate = *(struct voctrl_playback_state *)arg; + + update_playback_state(w32); + return VO_TRUE; + } + case VOCTRL_GET_DISPLAY_FPS: + update_display_info(w32); + *(double*) arg = w32->display_fps; + return VO_TRUE; + case VOCTRL_GET_DISPLAY_RES: ; + RECT monrc = get_monitor_info(w32).rcMonitor; + ((int *)arg)[0] = monrc.right - monrc.left; + ((int *)arg)[1] = monrc.bottom - monrc.top; + return VO_TRUE; + case VOCTRL_GET_DISPLAY_NAMES: + *(char ***)arg = get_disp_names(w32); + return VO_TRUE; + case VOCTRL_GET_ICC_PROFILE: + update_display_info(w32); + if (w32->color_profile) { + bstr *p = arg; + *p = stream_read_file(w32->color_profile, NULL, + w32->vo->global, 100000000); // 100 MB + return p->len ? VO_TRUE : VO_FALSE; + } + return VO_FALSE; + case VOCTRL_GET_FOCUSED: + *(bool *)arg = w32->focused; + return VO_TRUE; + } + return VO_NOTIMPL; +} + +static void do_control(void *ptr) +{ + void **p = ptr; + struct vo_w32_state *w32 = p[0]; + int *events = p[1]; + int request = *(int *)p[2]; + void *arg = p[3]; + int *ret = p[4]; + *ret = gui_thread_control(w32, request, arg); + *events |= atomic_fetch_and(&w32->event_flags, 0); + // Safe access, since caller (owner of vo) is blocked. + if (*events & VO_EVENT_RESIZE) { + w32->vo->dwidth = rect_w(w32->windowrc); + w32->vo->dheight = rect_h(w32->windowrc); + } +} + +int vo_w32_control(struct vo *vo, int *events, int request, void *arg) +{ + struct vo_w32_state *w32 = vo->w32; + if (request == VOCTRL_CHECK_EVENTS) { + *events |= atomic_fetch_and(&w32->event_flags, 0); + if (*events & VO_EVENT_RESIZE) { + mp_dispatch_lock(w32->dispatch); + vo->dwidth = rect_w(w32->windowrc); + vo->dheight = rect_h(w32->windowrc); + mp_dispatch_unlock(w32->dispatch); + } + return VO_TRUE; + } else { + int r; + void *p[] = {w32, events, &request, arg, &r}; + mp_dispatch_run(w32->dispatch, do_control, p); + return r; + } +} + +static void do_terminate(void *ptr) +{ + struct vo_w32_state *w32 = ptr; + w32->terminate = true; + + if (!w32->destroyed) + DestroyWindow(w32->window); + + mp_dispatch_interrupt(w32->dispatch); +} + +void vo_w32_uninit(struct vo *vo) +{ + struct vo_w32_state *w32 = vo->w32; + if (!w32) + return; + + mp_dispatch_run(w32->dispatch, do_terminate, w32); + mp_thread_join(w32->thread); + + AvRevertMmThreadCharacteristics(w32->avrt_handle); + + talloc_free(w32); + vo->w32 = NULL; +} + +HWND vo_w32_hwnd(struct vo *vo) +{ + struct vo_w32_state *w32 = vo->w32; + return w32->window; // immutable, so no synchronization needed +} + +void vo_w32_run_on_thread(struct vo *vo, void (*cb)(void *ctx), void *ctx) +{ + struct vo_w32_state *w32 = vo->w32; + mp_dispatch_run(w32->dispatch, cb, ctx); +} |