Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
C
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485/* * 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>
#define _DECL_DLLMAIN#include <windows.h>#include <windowsx.h>#include <dwmapi.h>#include <ole2.h>#include <process.h>#include <shellscalingapi.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 "win32/menu.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"
#define MPV_WINDOW_CLASS_NAME L"mpv"
EXTERN_C IMAGE_DOS_HEADER __ImageBase;#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
#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
#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 { 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;
struct menu_ctx *menu_ctx;
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 track_event;
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 tbtn_created_msg; bool tbtn_created;
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; bool dragging; bool start_dragging; BOOL win_arranging;
bool conversion_mode_init; bool unmaximize;};
static inline int get_system_metrics(struct vo_w32_state *w32, int metric){ return GetSystemMetricsForDpi(metric, w32->dpi);}
static void adjust_window_rect(struct vo_w32_state *w32, HWND hwnd, RECT *rc){ if (!w32->opts->border && !IsMaximized(w32->window)) return;
AdjustWindowRectExForDpi(rc, GetWindowLongPtrW(hwnd, GWL_STYLE), 0, GetWindowLongPtrW(hwnd, GWL_EXSTYLE), w32->dpi);}
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=nostatic int get_title_bar_height(struct vo_w32_state *w32){ assert(w32->opts->border ? !w32->opts->title_bar : IsMaximized(w32->window)); 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){ 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 = {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 (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=undefinedstatic 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){ 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; }
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){ 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, WPARAM wc, bool decode){ int c = decode ? decode_utf16(w32, wc) : 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 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 responsive. // 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);}
// If native touch is enabled and the mouse event is emulated, ignore it.// See: <https://learn.microsoft.com/en-us/windows/win32/tablet/// system-events-and-mouse-messages#distinguishing-pen-input-from-mouse-and-touch>static bool should_ignore_mouse_event(const struct vo_w32_state *w32){ return w32->opts->native_touch && ((GetMessageExtraInfo() & 0xFFFFFF00) == 0xFF515700);}
static void handle_mouse_down(struct vo_w32_state *w32, int btn, int x, int y){ if (should_ignore_mouse_event(w32)) return; btn |= mod_state(w32); mp_input_put_key(w32->input_ctx, btn | MP_KEY_STATE_DOWN); SetCapture(w32->window);}
static void handle_mouse_up(struct vo_w32_state *w32, int btn){ if (should_ignore_mouse_event(w32)) return; 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; wchar_t *wname = NULL;
HDC ic = CreateICW(device, NULL, NULL, NULL); if (!ic) goto done; 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;}
static void update_dpi(struct vo_w32_state *w32){ UINT dpiX, dpiY; HDC hdc = NULL; int dpi = 0;
if (GetDpiForMonitor(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->dpi / 96.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->tbtn_created) return;
if (!pstate->playing || !pstate->taskbar_progress) { ITaskbarList3_SetProgressState(w32->taskbar_list3, w32->window, TBPF_NOPROGRESS); return; }
ULONGLONG completed = pstate->position; ULONGLONG total = UINT8_MAX; if (!pstate->position) { completed = 1; total = MAXULONGLONG; }
ITaskbarList3_SetProgressValue(w32->taskbar_list3, w32->window, completed, total); 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 = 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;}
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 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 | 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 || w32->opts->window_maximized) ? 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) 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); 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(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 && (!w32->opts->window_maximized || w32->unmaximize)) { 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_window_state(struct vo_w32_state *w32);
static void update_maximized_state(struct vo_w32_state *w32, bool leaving_fullscreen){ if (w32->parent) return;
// Apply the maximized state on leaving fullscreen. if (w32->current_fs && !leaving_fullscreen) return;
bool toggle = w32->opts->window_maximized ^ IsMaximized(w32->window);
if (toggle && !w32->current_fs && w32->opts->window_maximized) w32->prev_windowrc = w32->windowrc;
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); } }
update_window_style(w32);
if (toggle && !w32->current_fs && !w32->opts->window_maximized) { w32->windowrc = w32->prev_windowrc; update_window_state(w32); }}
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);
// 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 && !w32->current_fs) { 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_SHOWMINNOACTIVE); update_maximized_state(w32, false); // 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
// 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 update_native_touch(const struct vo_w32_state *w32){ if (w32->parent) return;
if (w32->opts->native_touch) { RegisterTouchWindow(w32->window, 0); } else { UnregisterTouchWindow(w32->window); mp_input_put_key(w32->input_ctx, MP_TOUCH_RELEASE_ALL); }}
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){ 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; } // 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: 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; // 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 != 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; update_window_style(w32); 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_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->input_ctx, cmdt); } 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; } // All custom items must use ids of less than 0xF000. The context menu items are // also larger than WM_USER, which excludes SCF_ISSECURE. if (wParam > WM_USER && wParam < 0xF000) { 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->input_ctx, cmdt); return 0; } } 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, true)) return 0; break; case WM_UNICHAR: if (wParam == UNICODE_NOCHAR) { return TRUE; } else if (handle_char(w32, wParam, false)) { 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->track_event); 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://web.archive.org/web/20100821161603/ // 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; if (!should_ignore_mouse_event(w32)) mp_input_set_mouse_pos(w32->input_ctx, x, y); } break; } case WM_LBUTTONDOWN: handle_mouse_down(w32, MP_MBTN_LEFT, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); 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)); return TRUE; case WM_XBUTTONUP: handle_mouse_up(w32, HIWORD(wParam) == 1 ? MP_MBTN_BACK : MP_MBTN_FORWARD); return TRUE; case WM_DISPLAYCHANGE: force_update_display_info(w32); break; case WM_SETTINGCHANGE: update_dark_mode(w32); update_window_style(w32); update_window_state(w32); break; case WM_NCCALCSIZE: if (!w32->opts->border && !IsMaximized(w32->window)) return 0;
// Apparently removing WS_CAPTION disables some window animation, instead // just reduce non-client size to remove title bar. if (wParam && lParam && !w32->current_fs && !w32->parent && (w32->opts->border ? !w32->opts->title_bar : IsMaximized(w32->window)) && (GetWindowLongPtrW(w32->window, GWL_STYLE) & WS_CAPTION)) { // Remove all NC area on Windows 10 due to inability to control the // top bar height before Windows 11. if (!check_windows10_build(22000) && !IsMaximized(w32->window)) return 0; RECT r = {0}; adjust_window_rect(w32, w32->window, &r); NCCALCSIZE_PARAMS *p = (LPNCCALCSIZE_PARAMS)lParam; p->rgrc[0].top += r.top + get_title_bar_height(w32); } break; case WM_IME_STARTCOMPOSITION: { HIMC imc = ImmGetContext(w32->window); if (imc) { COMPOSITIONFORM cf = {.dwStyle = CFS_POINT, .ptCurrentPos = {0, 0}}; ImmSetCompositionWindow(imc, &cf); ImmReleaseContext(w32->window, imc); } break; } case WM_CREATE: // The IME can only be changed to alphanumeric input after it's initialized. // Unfortunately, there is no way to know when this happens, as // none of the WM_CREATE, WM_INPUTLANGCHANGE, or WM_IME_* messages work. // This works if the IME is initialized within a short time after // the window is created. Otherwise, fallback to setting alphanumeric mode on // the first keypress. SetTimer(w32->window, (UINT_PTR)WM_CREATE, 250, NULL); break; case WM_TIMER: if (wParam == WM_CREATE) { // Default to alphanumeric input when the IME is first initialized. set_ime_conversion_mode(w32, IME_CMODE_ALPHANUMERIC); KillTimer(w32->window, (UINT_PTR)WM_CREATE); return 0; } break; case WM_SHOWMENU: mp_win32_menu_show(w32->menu_ctx, w32->window); break; case WM_TOUCH: { UINT count = LOWORD(wParam); TOUCHINPUT *inputs = talloc_array_ptrtype(NULL, inputs, count); if (GetTouchInputInfo((HTOUCHINPUT)lParam, count, inputs, sizeof(TOUCHINPUT))) { for (UINT i = 0; i < count; i++) { TOUCHINPUT *ti = &inputs[i]; POINT pt = {TOUCH_COORD_TO_PIXEL(ti->x), TOUCH_COORD_TO_PIXEL(ti->y)}; ScreenToClient(w32->window, &pt); if (ti->dwFlags & TOUCHEVENTF_DOWN) mp_input_add_touch_point(w32->input_ctx, ti->dwID, pt.x, pt.y); if (ti->dwFlags & TOUCHEVENTF_MOVE) mp_input_update_touch_point(w32->input_ctx, ti->dwID, pt.x, pt.y); if (ti->dwFlags & TOUCHEVENTF_UP) mp_input_remove_touch_point(w32->input_ctx, ti->dwID); } } CloseTouchInputHandle((HTOUCHINPUT)lParam); talloc_free(inputs); return 0; } }
if (message == w32->tbtn_created_msg) { w32->tbtn_created = 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 = MPV_WINDOW_CLASS_NAME, });}
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);}
static bool is_key_message(UINT msg){ return msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN || msg == WM_KEYUP || msg == WM_SYSKEYUP;}
// 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 (!w32->destroyed && GetMessageW(&msg, 0, 0, 0) > 0) { // Change the conversion mode on the first keypress, in case the timer // solution fails. Note that this leaves the mode indicator in the language // bar showing the original mode until a key is pressed. if (is_key_message(msg.message) && !w32->conversion_mode_init) { set_ime_conversion_mode(w32, IME_CMODE_ALPHANUMERIC); w32->conversion_mode_init = true; KillTimer(w32->window, (UINT_PTR)WM_CREATE); } // Only send IME messages to TranslateMessage if (is_key_message(msg.message) && msg.wParam == VK_PROCESSKEY) TranslateMessage(&msg); DispatchMessageW(&msg); }
// Even if the message loop somehow exits, we still have to respond to // external requests until termination is requested. while (!w32->terminate) { assert(!w32->in_dispatch); w32->in_dispatch = true; mp_dispatch_queue_process(w32->dispatch, 1000); w32->in_dispatch = false; }}
static void window_reconfig(struct vo_w32_state *w32, bool force){ 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_geometry(vo, &screen, &mon, w32->dpi_scale, !w32->window_bounds_initialized, &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) || force;
w32->o_dwidth = vo->dwidth; w32->o_dheight = vo->dheight;
if (!w32->parent && (!w32->window_bounds_initialized || force)) { int x0 = geo.win.x0; int y0 = geo.win.y0; if (!w32->opts->geometry.xy_valid && w32->window_bounds_initialized) { x0 = w32->windowrc.left; y0 = w32->windowrc.top; } SetRect(&w32->windowrc, x0, y0, x0 + vo->dwidth, 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 = true; 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 || w32->opts->window_maximized) rc = &w32->prev_windowrc; w32->fit_on_screen = true; }
resize_and_move_rect(w32, rc, vo->dwidth, vo->dheight);
finish: reinit_window_state(w32);}
static void gui_thread_reconfig(void *ptr){ window_reconfig(ptr, false);}
// 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){ // 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. HMODULE uxtheme_dll = !check_windows10_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);
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), MPV_WINDOW_CLASS_NAME, 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), MPV_WINDOW_CLASS_NAME, 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; }
w32->menu_ctx = mp_win32_menu_init(w32->window); 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 (w32->opts->cursor_passthrough) update_cursor_passthrough(w32); if (w32->opts->native_touch) update_native_touch(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->tbtn_created_msg = RegisterWindowMessage(L"TaskbarButtonCreated"); } } } else { MP_ERR(w32, "Failed to initialize OLE/COM\n"); }
w32->tracking = FALSE; w32->track_event = (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->menu_ctx) mp_win32_menu_uninit(w32->menu_ctx); 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) { if (!vo_opts->fullscreen) update_maximized_state(w32, true); 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->cursor_passthrough) { update_cursor_passthrough(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->show_in_taskbar) { // This hide and show is apparently required according to the documentation: // https://learn.microsoft.com/en-us/windows/win32/shell/taskbar#managing-taskbar-buttons ShowWindow(w32->window, SW_HIDE); update_window_style(w32); ShowWindow(w32->window, SW_SHOW); 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, false); } else if (changed_option == &vo_opts->window_corners) { update_corners_pref(w32); } else if (changed_option == &vo_opts->native_touch) { update_native_touch(w32); } else if (changed_option == &vo_opts->geometry || changed_option == &vo_opts->autofit || changed_option == &vo_opts->autofit_smaller || changed_option == &vo_opts->autofit_larger) { if (w32->opts->window_maximized) { w32->unmaximize = true; } window_reconfig(w32, true); } }
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->opts->window_maximized) ? &w32->prev_windowrc : &w32->windowrc; s[0] = rect_w(*rc); s[1] = rect_h(*rc); return VO_TRUE; } case VOCTRL_SET_UNFS_WINDOW_SIZE: { int *s = arg;
if (!w32->window_bounds_initialized) return VO_FALSE;
RECT *rc = w32->current_fs ? &w32->prev_windowrc : &w32->windowrc; resize_and_move_rect(w32, rc, s[0], s[1]);
if (w32->opts->window_maximized && !w32->current_fs) { w32->unmaximize = true; } 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; case VOCTRL_BEGIN_DRAGGING: w32->start_dragging = true; return VO_TRUE; case VOCTRL_SHOW_MENU: PostMessageW(w32->window, WM_SHOWMENU, 0, 0); return VO_TRUE; case VOCTRL_UPDATE_MENU: mp_win32_menu_update(w32->menu_ctx, (struct mpv_node *)arg); 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);}
void vo_w32_set_transparency(struct vo *vo, bool enable){ struct vo_w32_state *w32 = vo->w32; if (w32->parent) return;
DWM_BLURBEHIND dbb = {0}; if (enable) { HRGN rgn = CreateRectRgn(0, 0, -1, -1); dbb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; dbb.hRgnBlur = rgn; dbb.fEnable = TRUE; DwmEnableBlurBehindWindow(w32->window, &dbb); DeleteObject(rgn); } else { dbb.dwFlags = DWM_BB_ENABLE; dbb.fEnable = FALSE; DwmEnableBlurBehindWindow(w32->window, &dbb); }}
BOOL WINAPI DllMain(HANDLE dll, DWORD reason, LPVOID reserved){ if (reason == DLL_PROCESS_DETACH && window_class) UnregisterClassW(MPV_WINDOW_CLASS_NAME, HINST_THISCOMPONENT); return TRUE;}