diff options
Diffstat (limited to 'video/out/w32_common.c')
-rw-r--r-- | video/out/w32_common.c | 838 |
1 files changed, 677 insertions, 161 deletions
diff --git a/video/out/w32_common.c b/video/out/w32_common.c index f228bc99b6..f7af24fcc3 100644 --- a/video/out/w32_common.c +++ b/video/out/w32_common.c @@ -15,10 +15,11 @@ * License along with mpv. If not, see <http://www.gnu.org/licenses/>. */ -#include <stdio.h> -#include <limits.h> -#include <pthread.h> #include <assert.h> +#include <limits.h> +#include <stdatomic.h> +#include <stdio.h> + #include <windows.h> #include <windowsx.h> #include <dwmapi.h> @@ -39,10 +40,10 @@ #include "w32_common.h" #include "win32/displayconfig.h" #include "win32/droptarget.h" +#include "win32/menu.h" #include "osdep/io.h" #include "osdep/threads.h" #include "osdep/w32_keyboard.h" -#include "osdep/atomic.h" #include "misc/dispatch.h" #include "misc/rendezvous.h" #include "mpv_talloc.h" @@ -54,6 +55,22 @@ EXTERN_C IMAGE_DOS_HEADER __ImageBase; #define WM_DPICHANGED (0x02E0) #endif +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +#ifndef DWMWA_VISIBLE_FRAME_BORDER_THICKNESS +#define DWMWA_VISIBLE_FRAME_BORDER_THICKNESS 37 +#endif + +#ifndef DWMWA_WINDOW_CORNER_PREFERENCE +#define DWMWA_WINDOW_CORNER_PREFERENCE 33 +#endif + +#ifndef DWMWA_SYSTEMBACKDROP_TYPE +#define DWMWA_SYSTEMBACKDROP_TYPE 38 +#endif + #ifndef DPI_ENUMS_DECLARED typedef enum MONITOR_DPI_TYPE { MDT_EFFECTIVE_DPI = 0, @@ -66,10 +83,14 @@ typedef enum MONITOR_DPI_TYPE { #define rect_w(r) ((r).right - (r).left) #define rect_h(r) ((r).bottom - (r).top) +#define WM_SHOWMENU (WM_USER + 1) + 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); + int (WINAPI *pGetSystemMetricsForDpi)(int nIndex, UINT dpi); + BOOLEAN (WINAPI *pShouldAppsUseDarkMode)(void); + DWORD (WINAPI *pSetPreferredAppMode)(DWORD mode); }; struct vo_w32_state { @@ -79,7 +100,7 @@ struct vo_w32_state { struct m_config_cache *opts_cache; struct input_ctx *input_ctx; - pthread_t thread; + mp_thread thread; bool terminate; struct mp_dispatch_queue *dispatch; // used to run stuff on the GUI thread bool in_dispatch; @@ -91,6 +112,8 @@ struct vo_w32_state { HHOOK parent_win_hook; HWINEVENTHOOK parent_evt_hook; + struct menu_ctx *menu_ctx; + HMONITOR monitor; // Handle of the current screen char *color_profile; // Path of the current screen's color profile @@ -121,7 +144,7 @@ struct vo_w32_state { atomic_uint event_flags; BOOL tracking; - TRACKMOUSEEVENT trackEvent; + TRACKMOUSEEVENT track_event; int mouse_x; int mouse_y; @@ -141,10 +164,12 @@ struct vo_w32_state { // 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; + UINT tbtn_created_msg; + bool tbtn_created; struct voctrl_playback_state current_pstate; @@ -152,15 +177,42 @@ struct vo_w32_state { double display_fps; bool moving; - bool snapped; + + 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; + bool dragging; + bool start_dragging; + BOOL win_arranging; + + bool conversion_mode_init; + bool unmaximize; }; -static void add_window_borders(struct vo_w32_state *w32, HWND hwnd, RECT *rc) +static inline int get_system_metrics(struct vo_w32_state *w32, int metric) +{ + return w32->api.pGetSystemMetricsForDpi + ? w32->api.pGetSystemMetricsForDpi(metric, w32->dpi) + : GetSystemMetrics(metric); +} + +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, @@ -170,6 +222,69 @@ static void add_window_borders(struct vo_w32_state *w32, HWND hwnd, RECT *rc) } } +static bool check_windows10_build(DWORD build) +{ + OSVERSIONINFOEXW osvi = { + .dwOSVersionInfoSize = sizeof(osvi), + .dwMajorVersion = HIBYTE(_WIN32_WINNT_WIN10), + .dwMinorVersion = LOBYTE(_WIN32_WINNT_WIN10), + .dwBuildNumber = build, + }; + + DWORD type = VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER; + + ULONGLONG mask = 0; + mask = VerSetConditionMask(mask, VER_MAJORVERSION, VER_GREATER_EQUAL); + mask = VerSetConditionMask(mask, VER_MINORVERSION, VER_GREATER_EQUAL); + mask = VerSetConditionMask(mask, VER_BUILDNUMBER, VER_GREATER_EQUAL); + + return VerifyVersionInfoW(&osvi, type, mask); +} + +// Get adjusted title bar height, only relevant for --title-bar=no +static int get_title_bar_height(struct vo_w32_state *w32) +{ + assert(!w32->opts->title_bar && w32->opts->border); + UINT visible_border = 0; + // Only available on Windows 11, check in case it's backported and breaks + // WM_NCCALCSIZE exception for Windows 10. + if (check_windows10_build(22000)) { + DwmGetWindowAttribute(w32->window, DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, + &visible_border, sizeof(visible_border)); + } + int top_bar = IsMaximized(w32->window) + ? get_system_metrics(w32, SM_CYFRAME) + + get_system_metrics(w32, SM_CXPADDEDBORDER) + : visible_border; + return top_bar; +} + +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 + // Keep the frame border. On Windows 10 the top border is not retained. + // It appears that DWM draws the title bar with its full height, extending + // outside the window area. Essentially, there is a bug in DWM, preventing + // the adjustment of the title bar height. This issue occurs when both the + // top and left client areas are non-zero in WM_NCCALCSIZE. If the left NC + // area is set to 0, the title bar is drawn correctly with the adjusted + // height. To mitigate this problem, set the top NC area to zero. The issue + // doesn't happen on Windows 11 or when DWM NC drawing is disabled with + // DWMWA_NCRENDERING_POLICY. We aim to avoid the manual drawing the border + // and want the DWM look and feel, so skip the top border on Windows 10. + // Also DWMWA_VISIBLE_FRAME_BORDER_THICKNESS is available only on Windows 11, + // so it would be hard to guess this size correctly on Windows 10 anyway. + if (w32->opts->border && !w32->opts->title_bar && !w32->current_fs && + (GetWindowLongPtrW(w32->window, GWL_STYLE) & WS_CAPTION)) + { + if (!check_windows10_build(22000) && !IsMaximized(w32->window)) + *rc = win; + rc->top = win.top - get_title_bar_height(w32); + } +} + // 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) { @@ -186,51 +301,58 @@ static LRESULT borderless_nchittest(struct vo_w32_state *w32, int x, int y) if (IsMaximized(w32->window)) return HTCLIENT; - POINT mouse = { x, y }; - ScreenToClient(w32->window, &mouse); + RECT rc; + if (!GetWindowRect(w32->window, &rc)) + return HTNOWHERE; - // The horizontal frame should be the same size as the vertical frame, - // since the NONCLIENTMETRICS structure does not distinguish between them - int frame_size = GetSystemMetrics(SM_CXFRAME) + - GetSystemMetrics(SM_CXPADDEDBORDER); - // The diagonal size handles are slightly wider than the side borders - int diagonal_width = frame_size * 2 + GetSystemMetrics(SM_CXBORDER); + POINT frame = {get_system_metrics(w32, SM_CXSIZEFRAME), + get_system_metrics(w32, SM_CYSIZEFRAME)}; + if (w32->opts->border) { + frame.x += get_system_metrics(w32, SM_CXPADDEDBORDER); + frame.y += get_system_metrics(w32, SM_CXPADDEDBORDER); + if (!w32->opts->title_bar) + rc.top -= get_system_metrics(w32, SM_CXPADDEDBORDER); + } + InflateRect(&rc, -frame.x, -frame.y); // Hit-test top border - if (mouse.y < frame_size) { - if (mouse.x < diagonal_width) + if (y < rc.top) { + if (x < rc.left) return HTTOPLEFT; - if (mouse.x >= rect_w(w32->windowrc) - diagonal_width) + if (x > rc.right) return HTTOPRIGHT; return HTTOP; } // Hit-test bottom border - if (mouse.y >= rect_h(w32->windowrc) - frame_size) { - if (mouse.x < diagonal_width) + if (y > rc.bottom) { + if (x < rc.left) return HTBOTTOMLEFT; - if (mouse.x >= rect_w(w32->windowrc) - diagonal_width) + if (x > rc.right) return HTBOTTOMRIGHT; return HTBOTTOM; } // Hit-test side borders - if (mouse.x < frame_size) + if (x < rc.left) return HTLEFT; - if (mouse.x >= rect_w(w32->windowrc) - frame_size) + 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(int v) +static int get_resize_border(struct vo_w32_state *w32, int v) { switch (v) { - case WMSZ_LEFT: return 3; - case WMSZ_TOP: return 2; - case WMSZ_RIGHT: return 3; - case WMSZ_BOTTOM: return 2; + 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; @@ -301,8 +423,8 @@ static void clear_keyboard_buffer(void) // 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 + // <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); @@ -313,7 +435,7 @@ 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 + // <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 @@ -385,10 +507,6 @@ static bool handle_appcommand(struct vo_w32_state *w32, UINT cmd) 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)); @@ -396,7 +514,8 @@ static void handle_key_down(struct vo_w32_state *w32, UINT vkey, UINT scancode) return; } - mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32) | MP_KEY_STATE_DOWN); + int state = w32->opts->native_keyrepeat ? 0 : MP_KEY_STATE_DOWN; + mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32) | state); } static void handle_key_up(struct vo_w32_state *w32, UINT vkey, UINT scancode) @@ -413,9 +532,9 @@ static void handle_key_up(struct vo_w32_state *w32, UINT vkey, UINT scancode) } } -static bool handle_char(struct vo_w32_state *w32, wchar_t wc) +static bool handle_char(struct vo_w32_state *w32, WPARAM wc, bool decode) { - int c = decode_utf16(w32, wc); + int c = decode ? decode_utf16(w32, wc) : wc; if (c == 0) return true; @@ -426,23 +545,33 @@ static bool handle_char(struct vo_w32_state *w32, wchar_t wc) return true; } +static void begin_dragging(struct vo_w32_state *w32) +{ + if (w32->current_fs || + mp_input_test_dragging(w32->input_ctx, w32->mouse_x, w32->mouse_y)) + return; + // Window dragging hack + ReleaseCapture(); + // The dragging model loop is entered at SendMessage() here. + // Unfortunately, the w32->current_fs value is stale because the + // input is handled in a different thread, and we cannot wait for + // an up-to-date value before entering the model loop if dragging + // needs to be kept resonsive. + // Workaround this by intercepting the loop in the WM_MOVING message, + // where the up-to-date value is available. + SystemParametersInfoW(SPI_GETWINARRANGING, 0, &w32->win_arranging, 0); + w32->dragging = true; + SendMessage(w32->window, WM_NCLBUTTONDOWN, HTCAPTION, 0); + w32->dragging = false; + SystemParametersInfoW(SPI_SETWINARRANGING, w32->win_arranging, 0, 0); + + mp_input_put_key(w32->input_ctx, MP_INPUT_RELEASE_ALL); +} + 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; } @@ -510,6 +639,10 @@ static double get_refresh_rate_from_gdi(const wchar_t *device) case 95: case 119: case 143: + case 164: + case 239: + case 359: + case 479: rv = (rv + 1) / 1.001; } @@ -519,18 +652,20 @@ static double get_refresh_rate_from_gdi(const wchar_t *device) static char *get_color_profile(void *ctx, const wchar_t *device) { char *name = NULL; + wchar_t *wname = 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)) + wname = talloc_array(NULL, wchar_t, MP_PATH_MAX); + if (!GetICMProfileW(ic, &(DWORD){ MP_PATH_MAX - 1 }, wname)) goto done; name = mp_to_utf8(ctx, wname); done: if (ic) DeleteDC(ic); + talloc_free(wname); return name; } @@ -556,7 +691,8 @@ static void update_dpi(struct vo_w32_state *w32) } w32->dpi = dpi; - w32->dpi_scale = w32->opts->hidpi_window_scale ? w32->dpi / 96.0 : 1.0; + w32->dpi_scale = w32->dpi / 96.0; + signal_events(w32, VO_EVENT_DPI); } static void update_display_info(struct vo_w32_state *w32) @@ -612,7 +748,7 @@ static void update_playback_state(struct vo_w32_state *w32) { struct voctrl_playback_state *pstate = &w32->current_pstate; - if (!w32->taskbar_list3 || !w32->tbtnCreated) + if (!w32->taskbar_list3 || !w32->tbtn_created) return; if (!pstate->playing || !pstate->taskbar_progress) { @@ -659,8 +795,15 @@ static HMONITOR get_default_monitor(struct vo_w32_state *w32) w32->opts->screen_id; // Handle --fs-screen=<all|default> and --screen=default - if (id < 0) - return MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY); + 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) @@ -688,10 +831,10 @@ 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) }; + const int x = get_system_metrics(w32, SM_XVIRTUALSCREEN); + const int y = get_system_metrics(w32, SM_YVIRTUALSCREEN); + return (RECT) { x, y, x + get_system_metrics(w32, SM_CXVIRTUALSCREEN), + y + get_system_metrics(w32, SM_CYVIRTUALSCREEN) }; } return get_monitor_info(w32).rcMonitor; } @@ -702,13 +845,27 @@ static RECT get_working_area(struct vo_w32_state *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 = false; + w32->snapped = 0; return false; } @@ -732,15 +889,7 @@ static bool snap_to_screen_edges(struct vo_w32_state *w32, RECT *rc) // Get the work area to let the window snap to taskbar wr = get_working_area(w32); - // Check for invisible borders and adjust the work area size - RECT frame = {0}; - if (DwmGetWindowAttribute(w32->window, DWMWA_EXTENDED_FRAME_BOUNDS, - &frame, sizeof(RECT)) == S_OK) { - wr.left -= frame.left - rect.left; - wr.top -= frame.top - rect.top; - wr.right += rect.right - frame.right; - wr.bottom += rect.bottom - frame.bottom; - } + 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 @@ -751,48 +900,68 @@ static bool snap_to_screen_edges(struct vo_w32_state *w32, RECT *rc) } int threshold = (w32->dpi * 16) / 96; - bool snapped = false; + 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) { - snapped = true; + w32->snapped_left = 1; OffsetRect(&rect, wr.left - rect.left, 0); } else if (abs(rect.right - wr.right) < threshold) { - snapped = true; + 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) { - snapped = true; + w32->snapped_top = 1; OffsetRect(&rect, 0, wr.top - rect.top); } else if (abs(rect.bottom - wr.bottom) < threshold) { - snapped = true; + w32->snapped_bottom = 1; OffsetRect(&rect, 0, wr.bottom - rect.bottom); } - if (!w32->snapped && snapped) { + if (!was_snapped && w32->snapped != 0) { w32->snap_dx = cursor.x - rc->left; w32->snap_dy = cursor.y - rc->top; } - w32->snapped = snapped; *rc = rect; return true; } +static bool is_high_contrast(void) +{ + HIGHCONTRAST hc = {sizeof(hc)}; + SystemParametersInfo(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0); + return hc.dwFlags & HCF_HIGHCONTRASTON; +} + static DWORD update_style(struct vo_w32_state *w32, DWORD style) { - const DWORD NO_FRAME = WS_OVERLAPPED | WS_MINIMIZEBOX; + const DWORD NO_FRAME = WS_OVERLAPPED | WS_MINIMIZEBOX | WS_THICKFRAME; const DWORD FRAME = WS_OVERLAPPEDWINDOW; - const DWORD FULLSCREEN = NO_FRAME | WS_SYSMENU; + 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; + if (!w32->opts->title_bar && is_high_contrast()) + style &= ~WS_CAPTION; } return style; } +static DWORD update_exstyle(struct vo_w32_state *w32, DWORD exstyle) +{ + exstyle &= ~(WS_EX_TOOLWINDOW); + if (!w32->opts->show_in_taskbar) + exstyle |= WS_EX_TOOLWINDOW; + return exstyle; +} + static void update_window_style(struct vo_w32_state *w32) { if (w32->parent) @@ -802,13 +971,38 @@ static void update_window_style(struct vo_w32_state *w32) // 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); + const DWORD exstyle = GetWindowLongPtrW(w32->window, GWL_EXSTYLE); SetWindowLongPtrW(w32->window, GWL_STYLE, update_style(w32, style)); + SetWindowLongPtrW(w32->window, GWL_EXSTYLE, update_exstyle(w32, exstyle)); 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(RECT *rc, long n_w, long n_h) +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); @@ -824,10 +1018,8 @@ static bool fit_rect_size(RECT *rc, long n_w, long n_h) n_w = n_h * o_asp; } - // Calculate new position and save the rect - const int x = rc->left + o_w / 2 - n_w / 2; - const int y = rc->top + o_h / 2 - n_h / 2; - SetRect(rc, x, y, x + n_w, y + n_h); + resize_and_move_rect(w32, rc, n_w, n_h); + return true; } @@ -839,7 +1031,11 @@ static void fit_window_on_screen(struct vo_w32_state *w32) if (w32->opts->border) subtract_window_borders(w32, w32->window, &screen); - bool adjusted = fit_rect_size(&w32->windowrc, rect_w(screen), rect_h(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 @@ -910,14 +1106,13 @@ static void update_minimized_state(struct vo_w32_state *w32) } } -static void update_maximized_state(struct vo_w32_state *w32) +static void update_maximized_state(struct vo_w32_state *w32, bool leaving_fullscreen) { 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) + // Apply the maximized state on leaving fullscreen. + if (w32->current_fs && !leaving_fullscreen) return; WINDOWPLACEMENT wp = { .length = sizeof wp }; @@ -947,6 +1142,16 @@ static bool is_visible(HWND window) 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) @@ -959,11 +1164,24 @@ static void update_window_state(struct vo_w32_state *w32) wr.left, wr.top, rect_w(wr), rect_h(wr), SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOOWNERZORDER); + // Unmaximize the window if a size change is requested because SetWindowPos + // doesn't change the window maximized state. + // ShowWindow(SW_SHOWNOACTIVATE) can't be used here because it tries to + // "restore" the window to its size before it's maximized. + if (w32->unmaximize) { + WINDOWPLACEMENT wp = { .length = sizeof wp }; + GetWindowPlacement(w32->window, &wp); + wp.showCmd = SW_SHOWNOACTIVATE; + wp.rcNormalPosition = wr; + SetWindowPlacement(w32->window, &wp); + w32->unmaximize = false; + } + // Show the window if it's not yet visible if (!is_visible(w32->window)) { if (w32->opts->window_minimized) { - ShowWindow(w32->window, SW_SHOWMINIMIZED); - update_maximized_state(w32); // Set the WPF_RESTORETOMAXIMIZED flag + ShowWindow(w32->window, SW_SHOWMINNOACTIVE); + update_maximized_state(w32, false); // Set the WPF_RESTORETOMAXIMIZED flag } else if (w32->opts->window_maximized) { ShowWindow(w32->window, SW_SHOWMAXIMIZED); } else { @@ -978,9 +1196,52 @@ static void update_window_state(struct vo_w32_state *w32) 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) @@ -988,6 +1249,7 @@ static void reinit_window_state(struct vo_w32_state *w32) // 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). @@ -1000,6 +1262,73 @@ static void reinit_window_state(struct vo_w32_state *w32) 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 + + // if pShouldAppsUseDarkMode is not available, just assume it to be true + const BOOL use_dark_mode = !is_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 void update_cursor_passthrough(const struct vo_w32_state *w32) +{ + if (w32->parent) + return; + + LONG_PTR exstyle = GetWindowLongPtrW(w32->window, GWL_EXSTYLE); + if (exstyle) { + if (w32->opts->cursor_passthrough) { + SetWindowLongPtrW(w32->window, GWL_EXSTYLE, exstyle | WS_EX_LAYERED | WS_EX_TRANSPARENT); + // This is required, otherwise the titlebar disappears. + SetLayeredWindowAttributes(w32->window, 0, 255, LWA_ALPHA); + } else { + SetWindowLongPtrW(w32->window, GWL_EXSTYLE, exstyle & ~(WS_EX_LAYERED | WS_EX_TRANSPARENT)); + } + } +} + +static void set_ime_conversion_mode(const struct vo_w32_state *w32, DWORD mode) +{ + if (w32->parent) + return; + + HIMC imc = ImmGetContext(w32->window); + if (imc) { + DWORD sentence_mode; + if (ImmGetConversionStatus(imc, NULL, &sentence_mode)) + ImmSetConversionStatus(imc, mode, sentence_mode); + ImmReleaseContext(w32->window, imc); + } +} + static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { @@ -1026,14 +1355,24 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, mp_dispatch_queue_process(w32->dispatch, 0); w32->in_dispatch = false; } + // Start window dragging if the flag is set by the voctrl. + // This is processed here to avoid blocking the dispatch queue. + if (w32->start_dragging) { + w32->start_dragging = false; + begin_dragging(w32); + } switch (message) { - case WM_ERASEBKGND: // no need to erase background separately - return 1; + 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); @@ -1042,19 +1381,30 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, signal_events(w32, VO_EVENT_WIN_STATE); update_display_info(w32); // if we moved between monitors - MP_DBG(w32, "move window: %d:%d\n", x, y); break; } case WM_MOVING: { w32->moving = true; RECT *rc = (RECT*)lParam; + // Prevent the window from being moved if the window dragging hack + // is active, and the window is currently in fullscreen. + if (w32->dragging && w32->current_fs) { + // Temporarily disable window arrangement to prevent aero shake + // from being activated. The original system setting will be restored + // after the dragging hack ends. + if (w32->win_arranging) { + SystemParametersInfoW(SPI_SETWINARRANGING, FALSE, 0, 0); + } + *rc = w32->windowrc; + return TRUE; + } if (snap_to_screen_edges(w32, rc)) return TRUE; break; } case WM_ENTERSIZEMOVE: w32->moving = true; - if (w32->snapped) { + if (w32->snapped != 0) { // Save the cursor offset from the window borders, // so the player window can be unsnapped later RECT rc; @@ -1069,9 +1419,6 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, w32->moving = false; break; case WM_SIZE: { - if (w32->moving) - w32->snapped = false; - const int w = LOWORD(lParam), h = HIWORD(lParam); if (w > 0 && h > 0) { w32->windowrc.right = w32->windowrc.left + w; @@ -1122,7 +1469,7 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, 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(wParam); + 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] }; @@ -1154,6 +1501,14 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, w32->window = NULL; PostQuitMessage(0); break; + case WM_COMMAND: { + const char *cmd = mp_win32_menu_get_cmd(w32->menu_ctx, LOWORD(wParam)); + if (cmd) { + mp_cmd_t *cmdt = mp_input_parse_cmd(w32->input_ctx, bstr0(cmd), ""); + mp_input_queue_cmd(w32-& |