#pragma once #include "platform.h" #include #include #include namespace ks { inline constexpr UINT ScrollActivity=WM_APP+42; inline constexpr UINT_PTR ScrollFrameTimer=0x4b5301, ScrollIdleTimer=0x4b5302; struct WheelBatch { long long vertical{}, horizontal{}; WORD keys{}; LPARAM point{}; bool frame{}, active{}, listBox{}; int remainder{}; }; inline LRESULT CALLBACK wheelBatchProc(HWND w,UINT msg,WPARAM wp,LPARAM lp,UINT_PTR id,DWORD_PTR data) { auto *batch=reinterpret_cast(data); if(msg==WM_MOUSEWHEEL || msg==WM_MOUSEHWHEEL) { (msg==WM_MOUSEWHEEL?batch->vertical:batch->horizontal)+=GET_WHEEL_DELTA_WPARAM(wp); batch->keys=GET_KEYSTATE_WPARAM(wp); batch->point=lp; if(!batch->active) { batch->active=true; SendMessageW(GetParent(w),ScrollActivity,(WPARAM)w,TRUE); } // Do not restart the frame timer: an uninterrupted wheel stream must // keep moving, rather than wait for a debounce interval to expire. if(!batch->frame) batch->frame=SetTimer(w,ScrollFrameTimer,16,nullptr)!=0; SetTimer(w,ScrollIdleTimer,100,nullptr); return 0; } if(msg==WM_TIMER && wp==ScrollFrameTimer) { KillTimer(w,ScrollFrameTimer); batch->frame=false; if(batch->listBox && batch->vertical) { UINT lines=3; SystemParametersInfoW(SPI_GETWHEELSCROLLLINES,0,&lines,0); if(lines==WHEEL_PAGESCROLL) { RECT client{}; GetClientRect(w,&client); auto height=SendMessageW(w,LB_GETITEMHEIGHT,0,0); lines=std::max(1,client.bottom/std::max(1,height)); } auto delta=batch->vertical+batch->remainder; batch->remainder=(int)(delta%WHEEL_DELTA); auto top=SendMessageW(w,LB_GETTOPINDEX,0,0); auto count=SendMessageW(w,LB_GETCOUNT,0,0); auto next=std::clamp(top-(delta/WHEEL_DELTA)*lines,0,std::max(0,count-1)); if(next!=top) SendMessageW(w,LB_SETTOPINDEX,(WPARAM)next,0); batch->vertical=0; } else if(batch->vertical) { // Native edit/list-view/window handlers retain their own scrolling // semantics, including system settings and partial wheel deltas. auto delta=(short)std::clamp(batch->vertical,-32760,32760); batch->vertical-=delta; DefSubclassProc(w,WM_MOUSEWHEEL,MAKEWPARAM(batch->keys,(WORD)delta),batch->point); } if(batch->horizontal) { auto delta=(short)std::clamp(batch->horizontal,-32760,32760); batch->horizontal-=delta; DefSubclassProc(w,WM_MOUSEHWHEEL,MAKEWPARAM(batch->keys,(WORD)delta),batch->point); } if(batch->vertical || batch->horizontal) batch->frame=SetTimer(w,ScrollFrameTimer,16,nullptr)!=0; return 0; } if(msg==WM_TIMER && wp==ScrollIdleTimer) { if(batch->frame) return 0; KillTimer(w,ScrollIdleTimer); batch->active=false; SendMessageW(GetParent(w),ScrollActivity,(WPARAM)w,FALSE); return 0; } if(msg==WM_NCDESTROY) { KillTimer(w,ScrollFrameTimer); KillTimer(w,ScrollIdleTimer); RemoveWindowSubclass(w,wheelBatchProc,id); delete batch; } return DefSubclassProc(w,msg,wp,lp); } inline void enableWheelBatching(HWND window,bool listBox=false) { if(!window) return; auto *batch=new WheelBatch; batch->listBox=listBox; if(!SetWindowSubclass(window,wheelBatchProc,ScrollFrameTimer,(DWORD_PTR)batch)) delete batch; } }