#include "app.h" #include "ui_geometry.h" #include "screenshot.h" #include #include #include namespace ks { struct InputRegressionAccess { static void setup(App &a) { a.hookBindings = {{Toggle,{MOD_CONTROL,VK_F12}}, {Case,{MOD_CONTROL|MOD_SHIFT,'U'}}}; } static void click() { MSLLHOOKSTRUCT m{}; m.flags=LLMHF_INJECTED; App::mouseProc(HC_ACTION,WM_LBUTTONDOWN,(LPARAM)&m); } }; } int main(int argc, char **argv) { if (argc == 2 && std::string(argv[1]) == "--update-check") { try { auto result = ks::runUpdate(false, {}); std::cout << "Live update check: " << ks::utf8(result.state) << " " << ks::utf8(result.version) << " " << ks::utf8(result.error) << '\n'; return result.state == L"current" || result.state == L"available" ? 0 : 1; } catch (const std::exception &e) { std::cerr << e.what() << '\n'; return 1; } } auto temp=std::filesystem::temp_directory_path()/(L"KSwitcher-input-tests-"+std::to_wstring(GetCurrentProcessId())); int checks=0; auto require=[&](bool ok,const char* text) { if(!ok) throw std::runtime_error(text); ++checks; }; try { INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES|ICC_LISTVIEW_CLASSES}; InitCommonControlsEx(&controls); { ks::App app(GetModuleHandleW(nullptr),true,temp); ks::InputRegressionAccess::setup(app); // Message-only window: no hooks, synthetic input, clipboard changes or visible editor. app.window=CreateWindowExW(0,L"STATIC",L"Input tests",0,0,0,0,0,HWND_MESSAGE,nullptr,nullptr,nullptr); require(app.window!=nullptr,"message-only test window"); for(int variant=0;variant<3;++variant) for(int icon=0;icon<15;++icon) for(int size:{22,44,55}) { auto handle=(HICON)LoadImageW(app.instance,MAKEINTRESOURCEW(icon<9?300+variant*10+icon:400+variant*10+icon-9),IMAGE_ICON,size,size,0); require(handle!=nullptr,"Lucide toolbar icon loads at standard and high DPI"); DestroyIcon(handle); } { ks::ScreenshotHistory history; std::vector marks; require(!history.undo(marks) && !history.redo(marks),"empty annotation history is safe"); history.remember(marks); marks.push_back({{20,20},{20,20},1,24}); history.remember(marks); marks.push_back({{44,20},{100,20},0,15,8,1,0}); require(history.undo(marks) && marks.size()==1 && history.canRedo(),"undo preserves state for redo"); require(history.undo(marks) && marks.empty(),"multiple undo steps restore empty canvas"); require(history.redo(marks) && marks[0].radius==24,"redo restores marker size"); require(history.redo(marks) && marks.size()==2 && marks[1].thickness==8 && marks[1].fromNumber==1, "redo restores arrow style and endpoint links"); require(!history.canRedo(),"redo becomes unavailable after last replay"); history.undo(marks); history.remember(marks); marks[0].radius=40; require(!history.canRedo() && !history.redo(marks),"new edit discards redo branch"); require(history.undo(marks) && marks[0].radius==24,"new branch remains undoable"); ks::ScreenshotHistory bounded; for(int i=0;i<110;++i) { bounded.remember(marks); marks[0].radius=i; } int count=0; while(bounded.undo(marks)) ++count; require(count==100,"annotation undo history stays bounded"); count=0; while(bounded.redo(marks)) ++count; require(count==100 && marks[0].radius==109,"bounded history can replay all retained edits"); } { HDC dc=CreateCompatibleDC(nullptr); BITMAPINFO info{}; info.bmiHeader={sizeof(BITMAPINFOHEADER),160,-100,1,32,BI_RGB}; void *pixels{}; auto bitmap=CreateDIBSection(dc,&info,DIB_RGB_COLORS,&pixels,nullptr,0); auto old=SelectObject(dc,bitmap); PatBlt(dc,0,0,160,100,WHITENESS); auto plain=ks::screenshotClip(dc,160,100,{}); auto marked=ks::screenshotClip(dc,160,100,{{{30,30},{30,30},1},{{60,70},{130,70},0}}); auto &bytes=marked.formats[0].data; BITMAPINFOHEADER header{}; memcpy(&header,bytes.data(),sizeof(header)); require(header.biWidth==160&&header.biHeight==-100,"capture preserves source resolution and orientation"); require(bytes.size()==sizeof(header)+160*100*4,"capture exports complete 32-bit DIB"); auto pixel=[&](int x,int y) { auto offset=sizeof(header)+(y*160+x)*4; return RGB(bytes[offset+2],bytes[offset+1],bytes[offset]); }; require(pixel(30,20)==RGB(225,42,50),"number badge renders in exported image"); require(pixel(90,70)==RGB(225,42,50),"arrow shaft renders in exported image"); require(pixel(5,5)==RGB(255,255,255),"unmarked pixels preserved"); ks::ScreenshotMark blueArrow{{20,50},{140,50},0}; blueArrow.color=RGB(0,80,220); ks::ScreenshotMark greenBadge{{30,30},{30,30},1}; greenBadge.color=RGB(0,200,80); auto colored=ks::screenshotClip(dc,160,100,{blueArrow,greenBadge}); auto colorAt=[&](int x,int y) { auto &b=colored.formats[0].data; auto at=sizeof(BITMAPINFOHEADER)+(y*160+x)*4; return RGB(b[at+2],b[at+1],b[at]); }; require(colorAt(70,50)==blueArrow.color && colorAt(132,50)==blueArrow.color, "arrow shaft and head use object color"); require(colorAt(30,20)==greenBadge.color,"badge fill uses object color"); ks::ScreenshotHistory recolors; std::vector coloredMarks{blueArrow,greenBadge}; recolors.remember(coloredMarks); coloredMarks[0].color=RGB(200,0,100); recolors.remember(coloredMarks); coloredMarks[1].color=RGB(255,255,0); require(recolors.undo(coloredMarks) && coloredMarks[1].color==greenBadge.color,"undo restores badge color"); require(recolors.undo(coloredMarks) && coloredMarks[0].color==blueArrow.color,"undo restores arrow color"); require(recolors.redo(coloredMarks) && coloredMarks[0].color==RGB(200,0,100),"redo restores arrow recoloring"); require(ks::cloneScreenshotMark(greenBadge,{greenBadge},160,100).color==greenBadge.color, "cloned badge retains customized color"); ks::ScreenshotMark thickArrow{{20,50},{140,50},0,15,12}; auto grip=ks::screenshotThicknessHandle(thickArrow); require(grip.x==80 && grip.y==72,"thickness grip sits beside arrow midpoint"); auto thicker=ks::dragScreenshotMark(thickArrow,ks::MarkPart::Thickness,grip,{grip.x,grip.y+4},160,100); require(thicker.thickness==20 && thicker.from.x==20 && thicker.to.x==140, "dragging thickness grip changes width without changing endpoints"); require(ks::dragScreenshotMark(thickArrow,ks::MarkPart::Thickness,grip,{grip.x+20,grip.y},160,100).thickness==12, "movement along arrow does not change thickness"); require(ks::dragScreenshotMark(thickArrow,ks::MarkPart::Thickness,grip,{grip.x,0},160,100).thickness==1, "thickness grip respects minimum width"); auto cloned=ks::cloneScreenshotMark(thickArrow,{},160,100); require(cloned.thickness==12 && cloned.from.x!=thickArrow.from.x && cloned.to.x-cloned.from.x==120, "cloned arrow is offset and preserves dimensions and style"); auto thick=ks::screenshotClip(dc,160,100,{thickArrow}); auto shaftEdge=sizeof(BITMAPINFOHEADER)+(54*160+40)*4; require(thick.formats[0].data[shaftEdge+2]==225 && thick.formats[0].data[shaftEdge+1]==42, "export renders edited arrow thickness"); thickArrow.thickness=1; auto thin=ks::screenshotClip(dc,160,100,{thickArrow}); require(thin.formats[0].data[shaftEdge+1]==255,"thin arrow leaves surrounding pixels untouched"); thickArrow.thickness=12; require(ks::hitScreenshotMark(thickArrow,{60,55},1)==ks::MarkPart::Body, "thick arrow can be selected along its visible edge"); require(ks::dragScreenshotMark(thickArrow,ks::MarkPart::End,{140,50},{145,70},160,100).thickness==12, "editing arrow geometry preserves thickness"); ks::ScreenshotMark badge{{60,50},{60,50},2}; auto clonedBadge=ks::cloneScreenshotMark(badge,{badge,{{90,20},{90,20},5}},160,100); require(clonedBadge.number==1 && clonedBadge.radius==15 && clonedBadge.from.x==76, "cloned badge fills first numbering gap and keeps diameter"); require(ks::nextScreenshotNumber({})==1,"empty capture starts numbering at one"); std::vector numbered{ {{10,10},{10,10},4},{{20,10},{20,10},1},{{30,10},{30,10},3},{{0,0},{100,0},0}}; require(ks::nextScreenshotNumber(numbered)==2,"new badge fills interior gap regardless of stacking order"); numbered.push_back({{40,10},{40,10},2}); require(ks::nextScreenshotNumber(numbered)==5,"full sequence advances past existing numbers"); numbered.erase(numbered.begin()+1); require(ks::nextScreenshotNumber(numbered)==1,"deleted first number is reused"); require(numbered[0].number==4 && numbered[1].number==3,"number allocation never renumbers existing objects"); std::vector linked{ {{30,50},{30,50},1}, {{130,50},{130,50},2}, {{30,50},{130,50},0}}; ks::linkScreenshotEndpoint(linked[2],true,{30,50},linked,6); ks::linkScreenshotEndpoint(linked[2],false,{130,50},linked,6); ks::resolveScreenshotLinks(linked); require(linked[2].fromNumber==1 && linked[2].toNumber==2 && linked[2].from.x==45 && linked[2].to.x==115, "arrow endpoints attach to badge rims"); auto clonedLinked=ks::cloneScreenshotMark(linked[2],linked,160,100); require(clonedLinked.fromNumber==0 && clonedLinked.toNumber==0,"cloned arrow does not retain original links"); auto widthLinked=ks::dragScreenshotMark(linked[2],ks::MarkPart::Thickness,{80,70},{80,74},160,100); require(widthLinked.fromNumber==1 && widthLinked.toNumber==2,"thickness handle preserves endpoint links"); linked[0].from.y=20; linked[0].to.y=20; linked[1].radius=25; ks::resolveScreenshotLinks(linked); require(linked[2].from.y>20 && linked[2].from.y<50 && linked[2].to.x<110, "linked arrows follow badge movement and resize"); auto detached=ks::dragScreenshotMark(linked[2],ks::MarkPart::Start,linked[2].from,{10,90},160,100); require(detached.fromNumber==0 && detached.toNumber==2,"endpoint drag detaches only that endpoint"); ks::linkScreenshotEndpoint(detached,true,{10,90},linked,6); require(detached.fromNumber==0,"dropping away from badges remains detached"); auto translated=ks::dragScreenshotMark(linked[2],ks::MarkPart::Body,{80,35},{85,40},160,100); require(translated.fromNumber==0 && translated.toNumber==0,"moving whole arrow detaches both endpoints"); auto lastPoint=linked[2].from; linked.erase(linked.begin()); ks::resolveScreenshotLinks(linked); require(linked[1].fromNumber==0 && linked[1].toNumber==2 && linked[1].from.x==lastPoint.x, "deleting badge detaches without moving orphaned endpoint"); linked.push_back({{10,10},{10,10},1}); ks::resolveScreenshotLinks(linked); require(linked[1].fromNumber==0,"reused number cannot reconnect a deleted link"); require(ks::hitScreenshotMark(badge,{60,50},20)==ks::MarkPart::Body,"badge center moves even with large hit tolerance"); require(ks::hitScreenshotMark(badge,{75,50},6)==ks::MarkPart::Radius,"badge rim exposes resize handle"); auto enlarged=ks::dragScreenshotMark(badge,ks::MarkPart::Radius,{75,50},{95,50},160,100); require(enlarged.radius==35 && enlarged.number==2,"badge resizes without changing its number"); auto large=ks::screenshotClip(dc,160,100,{enlarged}); const auto &largeBytes=large.formats[0].data; auto red=sizeof(BITMAPINFOHEADER)+(50*160+90)*4; require(largeBytes[red+2]==225 && largeBytes[red+1]==42,"export uses edited badge diameter"); require(ks::dragScreenshotMark(badge,ks::MarkPart::Radius,{75,50},{60,50},160,100).radius==8, "badge diameter has a usable minimum"); ks::ScreenshotMark arrow{{20,20},{100,60},0}; require(ks::hitScreenshotMark(arrow,{60,40},5)==ks::MarkPart::Body,"arrow shaft selects whole arrow"); require(ks::hitScreenshotMark(arrow,{100,60},5)==ks::MarkPart::End,"arrow tip selects endpoint"); require(ks::hitScreenshotMark(arrow,{60,80},5)==ks::MarkPart::None,"empty canvas misses arrow"); auto moved=ks::dragScreenshotMark(arrow,ks::MarkPart::Body,{60,40},{150,90},160,100); require(moved.to.x==159 && moved.to.y==99 && moved.to.x-moved.from.x==80 && moved.to.y-moved.from.y==40, "moving arrow preserves shape at image boundary"); auto stretched=ks::dragScreenshotMark(arrow,ks::MarkPart::End,{100,60},{140,80},160,100); require(stretched.from.x==20 && stretched.from.y==20 && stretched.to.x==140 && stretched.to.y==80, "endpoint drag leaves opposite endpoint fixed"); require(ks::screenshotClip(dc,160,100,{}).formats[0].data==plain.formats[0].data, "annotations do not modify original capture"); ks::ScreenshotMark highlight{{20,50},{120,50},0,15,24}; highlight.points={{20,50},{120,50}}; highlight.color=RGB(255,255,0); SetPixel(dc,60,50,RGB(0,0,0)); auto yellow=ks::screenshotClip(dc,160,100,{highlight}); auto readPixel=[](const ks::Clip &clip,int x,int y) { auto &b=clip.formats[0].data; auto offset=sizeof(BITMAPINFOHEADER)+(y*160+x)*4; return RGB(b[offset+2],b[offset+1],b[offset]); }; require(readPixel(yellow,40,50)==RGB(255,255,153),"highlight is translucent in exported image"); require(readPixel(yellow,60,50)==RGB(0,0,0),"highlight keeps black text legible"); require(readPixel(yellow,40,10)==RGB(255,255,255),"highlight leaves pixels outside brush unchanged"); highlight.points.push_back({20,50}); require(readPixel(ks::screenshotClip(dc,160,100,{highlight}),40,50)==RGB(255,255,153), "overlapping a stroke with itself does not accumulate opacity"); ks::ScreenshotHistory colorHistory; std::vector strokes{highlight}; colorHistory.remember(strokes); strokes[0].color=RGB(0,255,255); require(readPixel(ks::screenshotClip(dc,160,100,strokes),40,50)==RGB(153,255,255), "changing highlight color rerenders original stroke"); require(colorHistory.undo(strokes) && strokes[0].color==RGB(255,255,0),"undo restores highlight color"); require(colorHistory.redo(strokes) && strokes[0].color==RGB(0,255,255),"redo restores highlight recolor"); require(ks::hitScreenshotMark(highlight,{80,55},1)==ks::MarkPart::Body,"highlight stroke is selectable"); require(ks::hitScreenshotMark(highlight,{80,10},1)==ks::MarkPart::None,"highlight hit test excludes empty area"); auto movedHighlight=ks::dragScreenshotMark(highlight,ks::MarkPart::Body,{20,50},{30,60},160,100); require(movedHighlight.points[1].x==130 && movedHighlight.points[1].y==60,"moving highlight translates entire stroke"); auto cloneHighlight=ks::cloneScreenshotMark(highlight,{highlight},160,100); require(cloneHighlight.points[0].x!=highlight.points[0].x && cloneHighlight.color==highlight.color && cloneHighlight.thickness==24 && cloneHighlight.number==0,"cloning highlight preserves brush and color"); PatBlt(dc,0,0,160,100,WHITENESS); ks::ScreenshotMark line{{20,50},{140,50},0}; line.ends=ks::LineEnds::None; ks::ScreenshotMark textMark{{5,5},{5,5},0}; textMark.text=L"Hello &\r\nСвіт"; textMark.fontSize=14; textMark.color=RGB(0,80,180); auto textBounds=ks::screenshotTextBounds(textMark); require(textBounds.right>30 && textBounds.bottom>30,"multiline Unicode text is measured"); auto textImage=ks::screenshotClip(dc,160,100,{textMark}); int textInk=0; for(int y=5;y20,"text exports visible colored glyphs"); require(ks::hitScreenshotMark(textMark,{6,6},2)==ks::MarkPart::Body,"text is movable inside its bounds"); require(ks::hitScreenshotMark(textMark,{150,90},2)==ks::MarkPart::None,"text excludes empty canvas"); auto bigText=textMark; bigText.fontSize=28; auto bigBounds=ks::screenshotTextBounds(bigText); require(bigBounds.bottom>textBounds.bottom && bigBounds.right>textBounds.right,"font size scales text bounds"); auto serifText=textMark; serifText.fontName=L"Courier New"; serifText.fontWeight=FW_BOLD; serifText.fontItalic=true; require(ks::screenshotClip(dc,160,100,{serifText}).formats[0].data!=textImage.formats[0].data, "font family and style affect exported glyphs"); auto movedText=ks::dragScreenshotMark(textMark,ks::MarkPart::Body,{6,6},{16,16},160,100); auto rotatedText=textMark; rotatedText.rotation=90; auto rotatedBounds=ks::screenshotTextBounds(rotatedText); require(std::abs((rotatedBounds.right-rotatedBounds.left)-(textBounds.bottom-textBounds.top))<=1, "rotation transforms text bounds"); POINT center{(textBounds.left+textBounds.right)/2,(textBounds.top+textBounds.bottom)/2}; require(ks::hitScreenshotMark(rotatedText,center,2)==ks::MarkPart::Body,"rotated text hit test follows text"); auto rotationGrip=ks::screenshotTextRotationHandle(textMark); auto rotation=ks::dragScreenshotMark(textMark,ks::MarkPart::Rotation,rotationGrip,{center.x+40,center.y-25},160,100,true); require(std::abs(std::remainder(rotation.rotation,15))<0.001 && rotation.rotation!=0,"Shift snaps rotation to 15 degrees"); auto reverseRotation=ks::dragScreenshotMark(textMark,ks::MarkPart::Rotation,rotationGrip,{center.x-40,center.y-25},160,100,false); require(reverseRotation.rotation<0,"text rotates in the opposite direction"); require(ks::screenshotClip(dc,160,100,{rotatedText}).formats[0].data!=textImage.formats[0].data, "rotation is included in exported text pixels"); require(ks::cloneScreenshotMark(rotatedText,{rotatedText},160,100).rotation==90,"cloning preserves text rotation"); ks::ScreenshotMark labeled{{20,70},{140,70},0}; labeled.label=L"Label"; labeled.fontSize=12; auto label=ks::screenshotArrowLabel(labeled); auto labelBounds=ks::screenshotTextBounds(label); POINT labelCenter{(labelBounds.left+labelBounds.right)/2,(labelBounds.top+labelBounds.bottom)/2}; auto labelGrip=ks::screenshotTextRotationHandle(label); auto diagonal=labeled; diagonal.to={100,150}; for(int degrees:{0,45,90,135,180,-135,-90,-45}) { double angle=degrees*3.141592653589793/180; auto turning=labeled; turning.from={100,100}; turning.to={100+LONG(std::lround(80*std::cos(angle))),100+LONG(std::lround(80*std::sin(angle)))}; auto box=ks::screenshotTextBounds(ks::screenshotArrowLabel(turning)); double x=(box.left+box.right)/2.0-(turning.from.x+turning.to.x)/2.0; double y=(box.top+box.bottom)/2.0-(turning.from.y+turning.to.y)/2.0; require(std::abs(-x*std::sin(angle)+y*std::cos(angle)+24)<1.5, "caption keeps its perpendicular distance and side as line rotates"); require(std::abs(x*std::cos(angle)+y*std::sin(angle))<1.5, "caption remains centered along rotated line"); } auto beforeDrag=ks::screenshotTextBounds(ks::screenshotArrowLabel(diagonal)); auto offsetDiagonal=ks::dragScreenshotMark(diagonal,ks::MarkPart::Label,{0,0},{12,-8},200,200); require(!offsetDiagonal.labelRotationManual,"moving caption preserves automatic rotation"); auto movedCaptionSlope=ks::dragScreenshotMark(offsetDiagonal,ks::MarkPart::End,offsetDiagonal.to,{20,150},200,200); require(std::abs(ks::screenshotArrowLabel(movedCaptionSlope).rotation-90)<0.001, "repositioned caption continues following line angle"); auto afterDrag=ks::screenshotTextBounds(ks::screenshotArrowLabel(offsetDiagonal)); require(std::abs(afterDrag.left-beforeDrag.left-12)<=1 && std::abs(afterDrag.top-beforeDrag.top+8)<=1, "dragging diagonal caption follows pointer in screen coordinates"); require(std::abs(ks::screenshotArrowLabel(diagonal).rotation-45)<0.001,"caption defaults to line angle"); auto changedSlope=ks::dragScreenshotMark(diagonal,ks::MarkPart::End,diagonal.to,{20,150},200,200); require(std::abs(ks::screenshotArrowLabel(changedSlope).rotation-90)<0.001,"automatic caption follows endpoint movement"); auto reversed=diagonal; std::swap(reversed.from,reversed.to); require(std::abs(ks::screenshotArrowLabel(reversed).rotation-45)<0.001,"automatic caption remains upright on reversed line"); auto diagonalLabel=ks::screenshotArrowLabel(diagonal); auto diagonalGrip=ks::screenshotTextRotationHandle(diagonalLabel); auto clickOnly=ks::dragScreenshotMark(diagonal,ks::MarkPart::Rotation,diagonalGrip,diagonalGrip,200,200,true); require(!clickOnly.labelRotationManual,"clicking rotation handle without dragging preserves automatic angle"); auto manual=ks::dragScreenshotMark(diagonal,ks::MarkPart::Rotation,diagonalGrip,{diagonalGrip.x+12,diagonalGrip.y},200,200,false); require(manual.labelRotationManual && manual.rotation>45 && manual.rotation<90, "manual rotation starts at displayed automatic angle without jumping"); auto manualResized=ks::dragScreenshotMark(manual,ks::MarkPart::End,manual.to,{20,150},200,200); require(ks::screenshotArrowLabel(manualResized).rotation==manual.rotation,"manual angle survives line resizing"); require(ks::cloneScreenshotMark(manual,{manual},200,200).labelRotationManual,"clone preserves manual rotation mode"); ks::ScreenshotHistory autoAngleHistory; std::vector autoAngles{diagonal}; autoAngleHistory.remember(autoAngles); autoAngles[0]=manual; require(autoAngleHistory.undo(autoAngles) && !autoAngles[0].labelRotationManual,"undo restores automatic angle mode"); require(autoAngleHistory.redo(autoAngles) && autoAngles[0].labelRotationManual,"redo restores manual angle mode"); auto curvedCaption=diagonal; curvedCaption.freehand=true; curvedCaption.points={{20,70},{60,70},{60,110},{100,150}}; require(std::abs(ks::screenshotArrowLabel(curvedCaption).rotation-90)<0.001,"curve caption follows its middle segment"); auto rotatedCaption=ks::dragScreenshotMark(labeled,ks::MarkPart::Rotation,labelGrip, {labelCenter.x+35,labelCenter.y-20},160,100,true); require(rotatedCaption.rotation!=0 && std::abs(std::remainder(rotatedCaption.rotation,15))<0.001, "arrow caption rotation snaps to 15 degrees"); require(rotatedCaption.from.x==labeled.from.x && rotatedCaption.from.y==labeled.from.y && rotatedCaption.to.x==labeled.to.x && rotatedCaption.to.y==labeled.to.y && rotatedCaption.labelOffset.x==labeled.labelOffset.x && rotatedCaption.labelOffset.y==labeled.labelOffset.y, "rotating caption preserves arrow geometry and caption position"); require(ks::screenshotArrowLabel(rotatedCaption).rotation==rotatedCaption.rotation, "linked caption uses the regular text rotation"); require(ks::screenshotClip(dc,160,100,{rotatedCaption}).formats[0].data!=ks::screenshotClip(dc,160,100,{labeled}).formats[0].data, "caption rotation is exported"); require(ks::cloneScreenshotMark(rotatedCaption,{rotatedCaption},160,100).rotation==rotatedCaption.rotation, "cloning retains caption angle"); ks::ScreenshotHistory captionHistory; std::vector captionMarks{labeled}; captionHistory.remember(captionMarks); captionMarks[0]=rotatedCaption; require(captionHistory.undo(captionMarks) && captionMarks[0].rotation==0,"undo restores caption angle"); require(captionHistory.redo(captionMarks) && captionMarks[0].rotation==rotatedCaption.rotation,"redo restores caption angle"); require(ks::hitScreenshotMark(labeled,labelCenter,2)==ks::MarkPart::Label,"arrow label is independently draggable"); auto repositioned=ks::dragScreenshotMark(labeled,ks::MarkPart::Label,labelCenter,{labelCenter.x+10,labelCenter.y-8},160,100); require(repositioned.labelOffset.x==10 && repositioned.labelOffset.y==-32 && repositioned.from.x==20, "label drag changes only its offset"); auto movedArrow=ks::dragScreenshotMark(labeled,ks::MarkPart::Body,{20,70},{25,75},160,100); auto movedLabel=ks::screenshotArrowLabel(movedArrow); require(movedLabel.from.x==label.from.x+5 && movedLabel.from.y==label.from.y+5,"label follows arrow movement"); auto resizedArrow=ks::dragScreenshotMark(labeled,ks::MarkPart::End,labeled.to,{120,70},160,100); require(ks::screenshotArrowLabel(resizedArrow).from.x==label.from.x-10,"label follows arrow resizing"); auto noLabel=labeled; noLabel.label.clear(); require(ks::screenshotClip(dc,160,100,{labeled}).formats[0].data!=ks::screenshotClip(dc,160,100,{noLabel}).formats[0].data, "arrow label is included in clipboard export"); require(ks::cloneScreenshotMark(labeled,{labeled},160,100).label==labeled.label,"cloning retains arrow label"); ks::ScreenshotHistory annotationHistory; std::vector annotations{rotatedText,labeled}; annotationHistory.remember(annotations); annotations[0]=rotation; annotations[1]=repositioned; require(annotationHistory.undo(annotations) && annotations[0].rotation==90 && annotations[1].labelOffset.x==0, "undo restores rotation and label position"); require(annotationHistory.redo(annotations) && annotations[0].rotation==rotation.rotation && annotations[1].labelOffset.x==10, "redo restores rotation and label position"); require(movedText.from.x==15 && movedText.from.y==15 && movedText.text==textMark.text,"text movement preserves content"); auto oversizedText=textMark; oversizedText.fontSize=100; auto oversizedBounds=ks::screenshotTextBounds(oversizedText); require(oversizedBounds.right-oversizedBounds.left>160 && oversizedBounds.bottom-oversizedBounds.top>100, "large text exceeds both canvas dimensions"); for(int direction:{-1,1}) { auto moved=ks::dragScreenshotMark(oversizedText,ks::MarkPart::Body,{0,0},{direction*20,direction*20},160,100); require(moved.from.x==oversizedText.from.x+direction*20 && moved.from.y==oversizedText.from.y+direction*20, "oversized text moves in either direction on both axes"); auto limited=ks::dragScreenshotMark(oversizedText,ks::MarkPart::Body,{0,0},{direction*10000,direction*10000},160,100); auto bounds=ks::screenshotTextBounds(limited); require(bounds.right>=16 && bounds.left<=144 && bounds.bottom>=16 && bounds.top<=84, "oversized text remains selectable at canvas edges"); } auto clonedText=ks::cloneScreenshotMark(serifText,{serifText},160,100); require(clonedText.text==serifText.text && clonedText.fontName==serifText.fontName && clonedText.fontWeight==FW_BOLD && clonedText.fontItalic,"text cloning preserves font and content"); ks::ScreenshotHistory textHistory; std::vector texts{textMark}; textHistory.remember(texts); texts[0]=serifText; texts[0].text=L"Changed"; require(textHistory.undo(texts) && texts[0].text==textMark.text && texts[0].fontName==textMark.fontName, "undo restores text and font"); require(textHistory.redo(texts) && texts[0].text==L"Changed" && texts[0].fontItalic, "redo restores text edits"); for(auto shape:{ks::ScreenshotShape::Rectangle,ks::ScreenshotShape::Circle,ks::ScreenshotShape::Ellipse}) { ks::ScreenshotMark figure{{30,20},{90,80},0}; figure.shape=shape; if(shape==ks::ScreenshotShape::Ellipse) figure.to.x=130; figure.color=RGB(0,140,80); figure.thickness=4; auto image=ks::screenshotClip(dc,160,100,{figure}); int mid=(figure.from.x+figure.to.x)/2; require(readPixel(image,mid,20)==figure.color,"shape contour renders in chosen color"); require(readPixel(image,mid,50)==RGB(255,255,255),"shape interior remains transparent"); require(ks::hitScreenshotMark(figure,{mid,20},2)==ks::MarkPart::Body,"shape contour is movable"); require(ks::hitScreenshotMark(figure,{mid,50},2)==ks::MarkPart::None,"shape interior does not intercept objects"); auto resized=ks::dragScreenshotMark(figure,ks::MarkPart::End,figure.to,{100,85},160,100); require(resized.shape==shape && resized.to.x==100,"shape corner can resize object"); if(shape==ks::ScreenshotShape::Circle) require(resized.to.x-resized.from.x==resized.to.y-resized.from.y,"circle remains round when resized"); auto moved=ks::dragScreenshotMark(figure,ks::MarkPart::Body,{mid,20},{mid+5,25},160,100); require(moved.from.x==35 && moved.to.x==figure.to.x+5,"moving shape preserves dimensions"); auto duplicate=ks::cloneScreenshotMark(figure,{figure},160,100); require(duplicate.shape==shape && duplicate.color==figure.color,"cloning preserves shape and color"); ks::ScreenshotHistory history; std::vector figures{figure}; history.remember(figures); figures[0]=resized; require(history.undo(figures) && figures[0].to.x==figure.to.x,"shape resize can be undone"); require(history.redo(figures) && figures[0].to.x==resized.to.x,"shape resize can be redone"); } auto circleCorner=ks::constrainScreenshotShape(ks::ScreenshotShape::Circle,{60,60},{0,10},160,100); require(circleCorner.x==0 && circleCorner.y==0,"circle supports reverse drawing and canvas boundaries"); auto solidLine=ks::screenshotClip(dc,160,100,{line}); require(readPixel(solidLine,28,47)==RGB(255,255,255) && readPixel(solidLine,132,47)==RGB(255,255,255), "plain line has no arrowheads"); for(auto ends:{ks::LineEnds::End,ks::LineEnds::Start,ks::LineEnds::Both}) { line.ends=ends; auto image=ks::screenshotClip(dc,160,100,{line}); require((readPixel(image,28,47)==line.color)==(ends!=ks::LineEnds::End),"start arrowhead matches ends setting"); require((readPixel(image,132,47)==line.color)==(ends!=ks::LineEnds::Start),"end arrowhead matches ends setting"); } line.ends=ks::LineEnds::None; std::vector styleImages; ks::ScreenshotMark shaky{{20,50},{140,50},0}; shaky.freehand=true; shaky.ends=ks::LineEnds::Both; shaky.points={{20,50},{19,50},{40,50},{120,50},{141,50},{140,50}}; auto shakyImage=ks::screenshotClip(dc,160,100,{shaky}); require(readPixel(shakyImage,28,47)==shaky.color && readPixel(shakyImage,12,47)==RGB(255,255,255), "start arrowhead points outward despite initial mouse jitter"); require(readPixel(shakyImage,132,47)==shaky.color && readPixel(shakyImage,148,47)==RGB(255,255,255), "end arrowhead points outward despite final mouse jitter"); for(auto style:{ks::LineStyle::Solid,ks::LineStyle::Dotted,ks::LineStyle::Dashed,ks::LineStyle::DashDot}) { line.style=style; auto image=ks::screenshotClip(dc,160,100,{line}); int ink=0,gaps=0; for(int x=25;x<135;++x) { if(readPixel(image,x,50)==line.color) ++ink; else ++gaps; } require(ink>0 && (style==ks::LineStyle::Solid?gaps==0:gaps>0),"line pattern exports visible ink and expected gaps"); for(auto &previous:styleImages) require(previous!=image.formats[0].data,"line styles have distinct output"); styleImages.push_back(image.formats[0].data); } ks::ScreenshotMark curve{{20,80},{140,30},0}; curve.freehand=true; curve.ends=ks::LineEnds::None; curve.points={{20,80},{50,20},{90,70},{140,30}}; curve.color=RGB(0,80,220); auto curveImage=ks::screenshotClip(dc,160,100,{curve}); require(!curve.isHighlight() && readPixel(curveImage,50,20)==curve.color,"freehand line exports opaque curved path"); require(ks::hitScreenshotMark(curve,{50,20},3)==ks::MarkPart::Body,"curve hit test follows actual stroke"); auto curveMoved=ks::dragScreenshotMark(curve,ks::MarkPart::Body,{50,20},{55,25},160,100); require(curveMoved.points[1].x==55 && curveMoved.points[1].y==25,"curve movement preserves all intermediate points"); auto curveEnd=ks::dragScreenshotMark(curve,ks::MarkPart::End,curve.to,{150,40},160,100); require(curveEnd.points.back().x==150 && curveEnd.points.back().y==40 && curveEnd.points[1].y==20, "curve endpoint editing preserves intermediate shape"); curve.style=ks::LineStyle::DashDot; curve.ends=ks::LineEnds::Both; auto curveCopy=ks::cloneScreenshotMark(curve,{curve},160,100); require(curveCopy.freehand && curveCopy.style==curve.style && curveCopy.ends==curve.ends,"curve cloning preserves style and arrowheads"); ks::ScreenshotHistory lineHistory; std::vector lines{curve}; lineHistory.remember(lines); lines[0].style=ks::LineStyle::Dotted; lines[0].ends=ks::LineEnds::None; require(lineHistory.undo(lines) && lines[0].style==ks::LineStyle::DashDot && lines[0].ends==ks::LineEnds::Both, "undo restores line pattern and endpoints"); require(lineHistory.redo(lines) && lines[0].style==ks::LineStyle::Dotted && lines[0].ends==ks::LineEnds::None, "redo restores line pattern and endpoints"); SelectObject(dc,old); DeleteObject(bitmap); DeleteDC(dc); } auto event=[](DWORD vk,DWORD scan=0,bool own=false) { KBDLLHOOKSTRUCT k{}; k.vkCode=vk; k.scanCode=scan; k.flags=LLKHF_INJECTED|LLKHF_LOWER_IL_INJECTED; k.dwExtraInfo=own?ks::inputMarker():123; return k; }; auto commands=[&] { MSG m{}; int n=0; while(PeekMessageW(&m,app.window,WM_HOTKEY,WM_HOTKEY,PM_REMOVE)) ++n; return n; }; auto f12=event(VK_F12); require(!app.handleKeyboard(event(VK_LCONTROL),true,0),"remote modifier passes through"); require(app.handleKeyboard(f12,true,MOD_CONTROL),"remote Ctrl+F12 handled"); require(app.handleKeyboard(f12,true,MOD_CONTROL),"remote repeat suppressed"); require(commands()==1,"one command per press"); require(!app.handleKeyboard(event(VK_F12,0,true),false,0),"own key-up bypasses latch"); require(app.handleKeyboard(f12,true,MOD_CONTROL),"own key-up leaves remote latch intact"); require(commands()==0,"no repeat after own key-up"); require(app.handleKeyboard(f12,false,0),"remote release consumed after modifier release"); require(app.handleKeyboard(f12,true,MOD_CONTROL),"second remote press handled"); require(commands()==1,"second remote press dispatches"); app.handleKeyboard(f12,false,0); require(app.handleKeyboard(event('U'),true,MOD_CONTROL|MOD_SHIFT),"combined modifiers"); app.handleKeyboard(event('U'),false,0); require(!app.handleKeyboard(f12,true,MOD_CONTROL|MOD_SHIFT),"exact modifiers only"); app.resetInput(); app.handleKeyboard(event('A',0x1e),true,0); require(app.run.keys==L"a"||app.run.keys==L"A","external scan recorded with current CapsLock"); auto keys=app.run.keys; auto version=app.inputVersion.load(); for(WORD key:{WORD('V'),WORD(VK_SHIFT),WORD(VK_LEFT),WORD(VK_F15),WORD(0xE8)}) for(bool up:{false,true}) { auto i=ks::keyboardInput(key,up?KEYEVENTF_KEYUP:0); require(i.ki.dwExtraInfo==ks::inputMarker(),"all generated events tagged"); require(!app.handleKeyboard(event(key,0,true),!up,MOD_CONTROL),"own events bypass handling"); } require(app.run.keys==keys&&app.inputVersion==version,"own input preserves transaction and run"); app.handleKeyboard(event(VK_BACK),true,0); require(app.run.keys.empty(),"remote backspace recorded"); app.handleKeyboard(event('B'),true,MOD_SHIFT); require(!app.run.keys.empty(),"VK-only remote input resolves scan code"); require(app.inputVersion>version,"remote input invalidates pending transaction"); app.handleKeyboard(event(VK_PACKET,0x1e),true,0); require(app.run.keys.empty(),"Unicode packet is not a physical scan code"); app.handleKeyboard(event('A',0x1e),true,0); ks::InputRegressionAccess::click(); require(app.run.keys.empty(),"remote mouse resets recorded input"); auto physical=f12; physical.flags=0; physical.dwExtraInfo=ks::inputMarker(); require(app.handleKeyboard(physical,true,MOD_CONTROL),"physical hotkey still works"); app.handleKeyboard(physical,false,0); auto fitted=ks::fitWindowRect({-2500,-800,-500,1200},{0,0,1200,800}); require(fitted.left==0&&fitted.top==0&&fitted.right==1200&&fitted.bottom==800,"oversized disconnected-monitor window fits work area"); fitted=ks::fitWindowRect({2000,2000,2600,2400},{-1920,0,0,1080}); require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported"); ks::showSettings(app,false); auto settings=app.settingsWindow; require(settings&&!IsWindowVisible(settings),"settings layout tested without changing user focus"); for(auto pair:{std::pair{207,215},std::pair{207,232},std::pair{207,220},std::pair{232,230},std::pair{230,231},std::pair{231,253}}) { RECT first{},second{},overlap{}; GetWindowRect(GetDlgItem(settings,pair.first),&first); GetWindowRect(GetDlgItem(settings,pair.second),&second); require(!IntersectRect(&overlap,&first,&second),"capture hotkey and layout controls do not overlap"); } RECT frame{},client{},cancel{}; GetWindowRect(settings,&frame); MONITORINFO monitor{sizeof(monitor)}; GetMonitorInfoW(MonitorFromWindow(settings,MONITOR_DEFAULTTONEAREST),&monitor); require(frame.left>=monitor.rcWork.left&&frame.top>=monitor.rcWork.top&&frame.right<=monitor.rcWork.right&&frame.bottom<=monitor.rcWork.bottom,"initial settings frame fits active work area"); UINT dpi=GetDpiForWindow(settings); std::cout << "Hidden Settings layout checked at " << dpi << " DPI\n"; GetWindowRect(GetDlgItem(settings,IDCANCEL),&cancel); require(cancel.right-cancel.left==MulDiv(100,dpi,96),"controls use actual window DPI"); SetWindowPos(settings,nullptr,0,0,480,360,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE); SCROLLINFO scroll{sizeof(scroll),SIF_ALL}; GetScrollInfo(settings,SB_VERT,&scroll); require(scroll.nMax+1>(int)scroll.nPage,"small settings window has vertical scrolling"); GetScrollInfo(settings,SB_HORZ,&scroll); require(scroll.nMax+1>(int)scroll.nPage,"small settings window has horizontal scrolling"); SendMessageW(settings,WM_VSCROLL,SB_BOTTOM,0); SendMessageW(settings,WM_HSCROLL,SB_RIGHT,0); GetClientRect(settings,&client); GetWindowRect(GetDlgItem(settings,IDCANCEL),&cancel); MapWindowPoints(nullptr,settings,(POINT*)&cancel,2); require(cancel.left>=0&&cancel.top>=0&&cancel.right<=client.right&&cancel.bottom<=client.bottom,"bottom actions reachable by scrolling"); SendMessageW(settings,WM_VSCROLL,SB_TOP,0); SendMessageW(settings,WM_HSCROLL,SB_LEFT,0); SendMessageW(GetDlgItem(settings,IDCANCEL),WM_SETFOCUS,0,0); GetWindowRect(GetDlgItem(settings,IDCANCEL),&cancel); MapWindowPoints(nullptr,settings,(POINT*)&cancel,2); require(cancel.left>=0&&cancel.top>=0&&cancel.right<=client.right&&cancel.bottom<=client.bottom,"keyboard focus reveals off-screen action"); SetWindowPos(settings,nullptr,-20000,-20000,0,0,SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE); SendMessageW(settings,WM_DISPLAYCHANGE,0,0); GetWindowRect(settings,&frame); GetMonitorInfoW(MonitorFromWindow(settings,MONITOR_DEFAULTTONEAREST),&monitor); require(frame.left>=monitor.rcWork.left&&frame.top>=monitor.rcWork.top&&frame.right<=monitor.rcWork.right&&frame.bottom<=monitor.rcWork.bottom,"display change recovers off-screen window"); DestroyWindow(settings); DestroyWindow(app.window); app.window=nullptr; } std::filesystem::remove_all(temp); std::cout<<"Passed "<