diff --git a/native/entry_hold.h b/native/entry_hold.h index d1c9d3c..01cbb13 100644 --- a/native/entry_hold.h +++ b/native/entry_hold.h @@ -10,7 +10,7 @@ struct EntryHold { using Clock=std::chrono::steady_clock; enum class Action { Idle, Launcher, QuickPanel }; - static constexpr auto duration=std::chrono::milliseconds(600); + static constexpr auto quick_panel_delay=std::chrono::milliseconds(600); static constexpr auto ring_delay=std::chrono::milliseconds(100); bool pressed=false,fired=false; unsigned device=~0u,button=0; @@ -22,7 +22,7 @@ struct EntryHold { if(inside)last_inside=now; // Brief pointer jitter is tolerated, but leaving cannot activate a hold. if(!inside){if(now-last_inside>std::chrono::milliseconds(150))cancel();return Action::Idle;} - if(!fired&&now-started>=duration){fired=true;return Action::QuickPanel;} + if(!fired&&now-started>=quick_panel_delay){fired=true;return Action::QuickPanel;} return Action::Idle; } Action event(bool down,unsigned source,unsigned mouse_button,bool available,bool inside,Clock::time_point now){ @@ -30,14 +30,18 @@ struct EntryHold { if(!pressed&&available&&inside){pressed=true;fired=false;device=source;button=mouse_button;started=last_inside=now;} return Action::Idle; } - if(!pressed||source!=device||mouse_button!=button)return Action::Idle; + // Steam may omit the device on release; known other devices still cannot release this hold. + if(!pressed||(source!=device&&source!=~0u&&device!=~0u)||mouse_button!=button)return Action::Idle; auto action=tick(now,available,inside); if(action==Action::Idle&&pressed&&!fired&&available&&inside)action=Action::Launcher; cancel();return action; } float progress(Clock::time_point now)const{ if(!pressed||now-started(now-started).count()/.6f,0.f,1.f); + const float elapsed=std::chrono::duration(now-started-ring_delay).count(); + const float remaining=std::chrono::duration(quick_panel_delay-ring_delay).count(); + // A tiny nonzero value reveals the track at 100ms without jumping to 1/6 full. + return std::clamp(elapsed/remaining,.001f,1.f); } }; diff --git a/native/host.cpp b/native/host.cpp index 85cf8ce..7813975 100644 --- a/native/host.cpp +++ b/native/host.cpp @@ -172,7 +172,7 @@ static std::vector dock_icon(bool hover,bool active,float progress=0){ // Keep texture storage alive while OpenVR samples the preceding frame. Raw // overlay replacement during a hold can disturb the image and hover target. struct DockIconTexture { - GLuint textures[2]{};int slot=0; + GLuint textures[2]{};int slot=0;std::vector upload; void init(){ glGenTextures(2,textures); for(auto texture:textures){ @@ -187,7 +187,9 @@ struct DockIconTexture { const int next=1-slot;glBindTexture(GL_TEXTURE_2D,textures[next]); glPixelStorei(GL_UNPACK_ALIGNMENT,4);glPixelStorei(GL_UNPACK_ROW_LENGTH,0); glPixelStorei(GL_UNPACK_SKIP_PIXELS,0);glPixelStorei(GL_UNPACK_SKIP_ROWS,0); - glTexSubImage2D(GL_TEXTURE_2D,0,0,0,dock_canvas_width,dock_canvas_height,GL_RGBA,GL_UNSIGNED_BYTE,pixels.data()); + // Raster icons use top-left rows; OpenVR OpenGL textures use bottom-left. + merge_paint(upload,pixels.data(),dock_canvas_width,dock_canvas_height,0,static_cast(nullptr),true); + glTexSubImage2D(GL_TEXTURE_2D,0,0,0,dock_canvas_width,dock_canvas_height,GL_RGBA,GL_UNSIGNED_BYTE,upload.data()); glFinish();vr::Texture_t texture{reinterpret_cast(uintptr_t(textures[next])),vr::TextureType_OpenGL,vr::ColorSpace_Auto}; const auto error=overlays->SetOverlayTexture(overlay,&texture);glFlush(); if(error){std::cerr<<"Dock texture submission failed "<IsTrackedDeviceConnected(event.trackedDeviceIndex)),entry_pointer.inside,now)); + activate_entry(entry_hold.event(event.eventType==vr::VREvent_MouseButtonDown,event.trackedDeviceIndex,event.data.mouse.button,anchor_ok&&(event.trackedDeviceIndex==vr::k_unTrackedDeviceIndexInvalid||vr_system->IsTrackedDeviceConnected(event.trackedDeviceIndex)),entry_pointer.inside,std::chrono::steady_clock::now())); } } if(!anchor_ok)entry_pointer.leave(); const bool entry_connected=entry_hold.device==vr::k_unTrackedDeviceIndexInvalid||vr_system->IsTrackedDeviceConnected(entry_hold.device); - activate_entry(entry_hold.tick(now,anchor_ok&&entry_connected,entry_pointer.inside&&entry_pointer.device==entry_hold.device)); + activate_entry(entry_hold.tick(std::chrono::steady_clock::now(),anchor_ok&&entry_connected,entry_pointer.inside&&entry_pointer.device==entry_hold.device)); for(auto&[key,v]:views)v->stick_scrolling=false; entry_actions(button,anchor_ok); if(launcher_open){bool held=false;for(auto& hand:entry_buttons)for(auto& trigger:hand)held|=trigger.held;launcher_dismiss.observe(held);} @@ -690,7 +692,7 @@ int main(int argc,char** argv){ close_keyboard(false); } for(auto i=views.begin();i!=views.end();){auto& v=*i->second;if(v.closed){overlays->ClearOverlayTexture(v.overlay);overlays->DestroyOverlay(v.overlay);if(v.thumbnail)overlays->DestroyOverlay(v.thumbnail);glDeleteTextures(2,v.textures);i=views.erase(i);}else ++i;} - bool hovered=anchor_ok&&(entry_hold.pressed||overlays->IsHoverTargetOverlay(button));if(hovered&&!icon_hover)hover_haptic(button,button_haptic_at);const auto badge=notification_badge_count.load();const int progress=std::lround(entry_hold.progress(now)*120);if(icon_progress!=progress||icon_active!=(menu_open||launcher_open)||icon_hover!=hovered||icon_badge!=badge){pixels=dock_icon(hovered,menu_open||launcher_open,progress/120.f);if(icon_texture.submit(button,pixels)){icon_progress=progress;icon_badge=badge;icon_active=menu_open||launcher_open;}}icon_hover=hovered; + bool hovered=anchor_ok&&(entry_hold.pressed||overlays->IsHoverTargetOverlay(button));if(hovered&&!icon_hover)hover_haptic(button,button_haptic_at);const auto badge=notification_badge_count.load();const float ring=entry_hold.progress(std::chrono::steady_clock::now());const int progress=ring>0?std::max(1,int(std::lround(ring*120))):0;if(icon_progress!=progress||icon_active!=(menu_open||launcher_open)||icon_hover!=hovered||icon_badge!=badge){pixels=dock_icon(hovered,menu_open||launcher_open,progress/120.f);if(icon_texture.submit(button,pixels)){icon_progress=progress;icon_badge=badge;icon_active=menu_open||launcher_open;}}icon_hover=hovered; auto sync=overlays->WaitFrameSync(8); // A timeout already waited: never append another sleep to it. Bound retries // only for immediate API failures, so a broken sync call cannot busy-spin. diff --git a/tests/entry_hold.cpp b/tests/entry_hold.cpp index 5e5e405..605b56b 100644 --- a/tests/entry_hold.cpp +++ b/tests/entry_hold.cpp @@ -1,4 +1,5 @@ #include "entry_hold.h" +#include "paint_buffer.h" #include #include @@ -16,9 +17,11 @@ int main(){ EntryHold hold; hold.event(true,1,1,true,true,start); assert(hold.progress(start+milliseconds(99))==0); - assert(std::abs(hold.progress(start+milliseconds(100))-1.f/6)<.001f); - assert(std::abs(hold.progress(start+milliseconds(300))-.5f)<.001f); + assert(hold.tick(start+milliseconds(100),true,true)==A::Idle); + assert(hold.progress(start+milliseconds(100))>0&&hold.progress(start+milliseconds(100))<.01f); + assert(std::abs(hold.progress(start+milliseconds(300))-.4f)<.001f); assert(hold.tick(start+milliseconds(599),true,true)==A::Idle); + assert(hold.progress(start+milliseconds(599))<1); assert(hold.tick(start+milliseconds(600),true,true)==A::QuickPanel); assert(hold.tick(start+milliseconds(900),true,true)==A::Idle); assert(hold.event(false,1,1,true,true,start+seconds(1))==A::Idle); @@ -30,6 +33,10 @@ int main(){ assert(hold.event(false,2,1,true,true,start+milliseconds(100))==A::Idle); assert(hold.event(false,1,2,true,true,start+milliseconds(110))==A::Idle); assert(hold.event(false,1,1,true,true,start+milliseconds(120))==A::Launcher); + // A release with no tracked device must not leave an old progress ring held. + hold.event(true,1,1,true,true,start); + assert(hold.event(false,~0u,1,true,true,start+milliseconds(80))==A::Launcher); + assert(!hold.pressed&&hold.progress(start+milliseconds(100))==0); // Small jitter does not restart the timer; a sustained leave or loss cancels. hold.event(true,1,1,true,true,start); hold.tick(start+milliseconds(480),true,true); @@ -63,6 +70,13 @@ int main(){ assert(pixels[top]>original[top]&&pixels[top]<255); assert(pixels[top]==pixels[top+1]&&pixels[top+1]==pixels[top+2]); assert(pixels[(8*128+64)*4]>original[(8*128+64)*4]); + // The GPU upload must flip top-left artwork exactly once, including the ring. + struct Rect{int x,y,width,height;}; + std::vector upload; + merge_paint(upload,pixels.data(),128,128,0,static_cast(nullptr),true); + assert(upload[((127-5)*128+64)*4]==pixels[top]); + assert(upload[((127-8)*128+64)*4]==pixels[(8*128+64)*4]); + assert(pixels[(5*128+64)*4]>pixels[(5*128+63)*4]); auto partial=pixels;entry_progress_ring(pixels,1); assert(pixels!=partial); std::cout<<"entry_hold: passed\n";