struct LanGridMetrics { int left=-1,right=1,top=-1,bottom=1,width{},height{}; RECT cell(int x,int y)const{return {(x-left)*width/(right-left+1),(y-top)*height/(bottom-top+1),(x-left+1)*width/(right-left+1),(y-top+1)*height/(bottom-top+1)};} POINT point(POINT p)const{return {std::clamp(left+int(p.x)*(right-left+1)/std::max(1,width),left,right),std::clamp(top+int(p.y)*(bottom-top+1)/std::max(1,height),top,bottom)};} }; LanGridMetrics lanGridMetrics(HWND w,const LanView &v) { RECT r{};GetClientRect(w,&r);LanGridMetrics g;g.width=r.right;g.height=r.bottom; const auto positions=v.snapshot.value("positions",nlohmann::json::object()); for(auto &[id,cell]:positions.items()){ int x=cell[0],y=cell[1];g.left=std::min(g.left,x-1);g.right=std::max(g.right,x+1);g.top=std::min(g.top,y-1);g.bottom=std::max(g.bottom,y+1); } g.left=std::max(-8,g.left);g.right=std::min(8,g.right);g.top=std::max(-8,g.top);g.bottom=std::min(8,g.bottom);return g; } void moveLanMonitor(HWND parent,LanView &v,const std::string &peer,int x,int y) { try{lanRequest(*v.app,{{"action","position"},{"peer",peer},{"x",x},{"y",y}});v.error.clear();} catch(const std::exception &e){v.error=wide(e.what());} refreshLan(parent,v); } LRESULT CALLBACK lanGridProc(HWND w,UINT msg,WPARAM wp,LPARAM lp) { auto *v=reinterpret_cast(GetWindowLongPtrW(w,GWLP_USERDATA)); if(msg==WM_NCCREATE){v=static_cast(reinterpret_cast(lp)->lpCreateParams);SetWindowLongPtrW(w,GWLP_USERDATA,reinterpret_cast(v));} if(!v)return DefWindowProcW(w,msg,wp,lp); auto parent=GetParent(w); if(msg==WM_ERASEBKGND)return 1; if(msg==WM_GETDLGCODE)return DLGC_WANTARROWS; if(msg==WM_SETFOCUS||msg==WM_KILLFOCUS){InvalidateRect(w,nullptr,FALSE);return 0;} if(msg==WM_LBUTTONDOWN){ SetFocus(w);POINT p{GET_X_LPARAM(lp),GET_Y_LPARAM(lp)};auto g=lanGridMetrics(w,*v); const auto positions=v->snapshot.value("positions",nlohmann::json::object()); for(auto &[id,cell]:positions.items()){ auto r=g.cell(cell[0],cell[1]);if(!PtInRect(&r,p))continue; auto found=std::find(v->ids.begin(),v->ids.end(),id);if(found==v->ids.end())break; SendDlgItemMessageW(parent,702,LB_SETCURSEL,std::distance(v->ids.begin(),found),0); v->dragPeer=id;v->dragStart=p;v->dragPoint=p;v->dragMoved=false;SetCapture(w);refreshLan(parent,*v);break; } return 0; } if(msg==WM_MOUSEMOVE&&!v->dragPeer.empty()){ v->dragPoint={GET_X_LPARAM(lp),GET_Y_LPARAM(lp)}; if(std::abs(v->dragPoint.x-v->dragStart.x)>scale(w,4)||std::abs(v->dragPoint.y-v->dragStart.y)>scale(w,4))v->dragMoved=true; InvalidateRect(w,nullptr,FALSE);return 0; } if(msg==WM_LBUTTONUP&&!v->dragPeer.empty()){ auto peer=v->dragPeer;auto p=lanGridMetrics(w,*v).point({GET_X_LPARAM(lp),GET_Y_LPARAM(lp)});bool moved=v->dragMoved; RECT client{};GetClientRect(w,&client);POINT mouse{GET_X_LPARAM(lp),GET_Y_LPARAM(lp)}; v->dragPeer.clear();v->dragMoved=false;ReleaseCapture(); if(moved&&PtInRect(&client,mouse))moveLanMonitor(parent,*v,peer,p.x,p.y); InvalidateRect(w,nullptr,FALSE);return 0; } if(msg==WM_CAPTURECHANGED){v->dragPeer.clear();v->dragMoved=false;InvalidateRect(w,nullptr,FALSE);return 0;} if(msg==WM_KEYDOWN){ if(wp==VK_ESCAPE&&!v->dragPeer.empty()){v->dragPeer.clear();ReleaseCapture();InvalidateRect(w,nullptr,FALSE);return 0;} auto peer=selectedLan(parent,*v);auto positions=v->snapshot.value("positions",nlohmann::json::object()); if(positions.contains(peer)&&(wp==VK_LEFT||wp==VK_RIGHT||wp==VK_UP||wp==VK_DOWN)){ int x=positions[peer][0],y=positions[peer][1];x+=(wp==VK_RIGHT)-(wp==VK_LEFT);y+=(wp==VK_DOWN)-(wp==VK_UP); moveLanMonitor(parent,*v,peer,x,y);return 0; } } if(msg==WM_PAINT){ PAINTSTRUCT ps{};auto target=BeginPaint(w,&ps);RECT bounds{};GetClientRect(w,&bounds); auto dc=CreateCompatibleDC(target);auto bitmap=CreateCompatibleBitmap(target,std::max(1L,bounds.right),std::max(1L,bounds.bottom));auto oldBitmap=SelectObject(dc,bitmap); auto fill=CreateSolidBrush(RGB(247,249,251));FillRect(dc,&bounds,fill);DeleteObject(fill); auto oldFont=SelectObject(dc,GetPropW(parent,L"Font"));SetBkMode(dc,TRANSPARENT); auto gridPen=CreatePen(PS_DOT,1,RGB(214,222,230));auto oldPen=SelectObject(dc,gridPen);auto oldBrush=SelectObject(dc,GetStockObject(NULL_BRUSH)); auto g=lanGridMetrics(w,*v); for(int x=g.left;x<=g.right+1;++x){int at=(x-g.left)*g.width/(g.right-g.left+1);MoveToEx(dc,at,0,nullptr);LineTo(dc,at,g.height);} for(int y=g.top;y<=g.bottom+1;++y){int at=(y-g.top)*g.height/(g.bottom-g.top+1);MoveToEx(dc,0,at,nullptr);LineTo(dc,g.width,at);} auto selected=selectedLan(parent,*v); auto draw=[&](const std::string &id,const std::wstring &name,int x,int y,bool local){ auto cell=g.cell(x,y);int margin=scale(w,8),cellWidth=cell.right-cell.left; TEXTMETRICW metrics{};GetTextMetricsW(dc,&metrics); RECT title{cell.left+scale(w,3),cell.bottom-scale(w,3)-metrics.tmHeight,cell.right-scale(w,3),cell.bottom-scale(w,3)}; int top=cell.top+scale(w,4),height=std::min(scale(w,36),int(title.top)-top-scale(w,8)); int width=std::min({scale(w,110),cellWidth-2*margin,height*2}); if(width<8||height<8)return; int left=(cell.left+cell.right-width)/2; bool active=!local&&v->snapshot.value("shareInput",false)&&v->snapshot.value("controlMode",0)!=0&&v->snapshot.value("controlPeer",std::string{})==id; COLORREF color=id==selected?RGB(0,105,190):active?RGB(30,130,90):RGB(105,120,133); auto pen=CreatePen(PS_SOLID,scale(w,2),color);SelectObject(dc,pen); auto brush=CreateSolidBrush(local?RGB(220,237,252):RGB(255,255,255));SelectObject(dc,brush); RoundRect(dc,left,top,left+width,top+height,scale(w,4),scale(w,4));SelectObject(dc,GetStockObject(NULL_BRUSH));DeleteObject(brush); int center=left+width/2;MoveToEx(dc,center,top+height,nullptr);LineTo(dc,center,top+height+scale(w,5)); MoveToEx(dc,center-scale(w,12),top+height+scale(w,5),nullptr);LineTo(dc,center+scale(w,12),top+height+scale(w,5)); RECT screen{left+scale(w,3),top+scale(w,2),left+width-scale(w,3),top+height-scale(w,2)};SetTextColor(dc,color); const wchar_t *caption=local?L"Local":active?L"Active":L"Paired"; if(height>=metrics.tmHeight+scale(w,4))DrawTextW(dc,caption,-1,&screen,DT_CENTER|DT_VCENTER|DT_SINGLELINE|DT_END_ELLIPSIS); SetTextColor(dc,RGB(30,40,50)); DrawTextW(dc,name.c_str(),-1,&title,DT_CENTER|DT_SINGLELINE|DT_END_ELLIPSIS|DT_NOPREFIX); if(id==selected){RECT outline{cell.left+scale(w,2),cell.top+scale(w,2),cell.right-scale(w,2),cell.bottom-scale(w,2)};if(GetFocus()==w)DrawFocusRect(dc,&outline);} SelectObject(dc,gridPen);DeleteObject(pen); }; draw("",wide(v->snapshot.value("name",std::string("This PC"))),0,0,true); for(const auto &peer:v->snapshot.value("peers",nlohmann::json::array())){ auto id=peer.at("id").get();auto positions=v->snapshot.value("positions",nlohmann::json::object()); if(positions.contains(id))draw(id,wide(peer.at("name").get()),positions[id][0],positions[id][1],false); } if(v->dragMoved&&!v->dragPeer.empty()){ auto point=g.point(v->dragPoint);auto r=g.cell(point.x,point.y);bool valid=true; try{lan::validateCell(v->snapshot.at("positions"),v->dragPeer,point.x,point.y);}catch(...){valid=false;} auto pen=CreatePen(PS_SOLID,scale(w,2),valid?RGB(0,105,190):RGB(190,50,40));SelectObject(dc,pen); Rectangle(dc,r.left+2,r.top+2,r.right-2,r.bottom-2);SelectObject(dc,gridPen);DeleteObject(pen); } SelectObject(dc,oldPen);SelectObject(dc,oldBrush);SelectObject(dc,oldFont);DeleteObject(gridPen); BitBlt(target,0,0,bounds.right,bounds.bottom,dc,0,0,SRCCOPY);SelectObject(dc,oldBitmap);DeleteObject(bitmap);DeleteDC(dc);EndPaint(w,&ps);return 0; } return DefWindowProcW(w,msg,wp,lp); }