#include "include/capi/cef_dialog_handler_capi.h" #include "include/capi/cef_app_capi.h" #include "include/capi/cef_image_capi.h" #include "include/capi/cef_parser_capi.h" #include "include/capi/cef_values_capi.h" #include "include/capi/cef_client_capi.h" #include "include/cef_api_hash.h" #include "vendor/json.hpp" #include "dock_uv_geometry.h" #include "menu_geometry.h" #include "menu_input.h" #include "entry_hold.h" #include "launcher_input.h" #include "launcher_fade.h" #include "launcher_priority.h" #include "launcher_pointer.h" #include "launcher_gaze.h" #include "gaze_geometry.h" #include "page_scroll.h" #include "page_scroll_injection.h" #include "include/capi/cef_load_handler_capi.h" #include #include "overlay_placement.h" #include "render_resolution.h" #include "render_timing.h" #include "paint_buffer.h" #include "notification_geometry.h" #include "notification_badge.h" #include "keyboard_state.h" #include "keyboard_input.h" #include "brand_mask.h" #include "ui_visibility.h" #include "ui_feedback.h" #include "plugin_navigation.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using json=nlohmann::json; static volatile sig_atomic_t running=1; static void stop(int){running=0;} static std::string origin,runtime,token; static vr::IVROverlay* overlays=nullptr; static vr::IVRSystem* vr_system=nullptr; static Display* xdisplay=nullptr; static GLXContext glcontext=nullptr; static GLXPbuffer glsurface=0; static std::atomic notification_badge_count{0}; static bool launcher_open=false; static bool ui_haptics=true,ui_sounds=false; static SteamUiSounds ui_audio; static LauncherFade launcher_fade; static std::chrono::steady_clock::time_point launcher_close_at{}; static LauncherDismissGuard launcher_dismiss; static LauncherDockGuard launcher_dock; static EntryTrigger launcher_close_button; static LauncherStick launcher_stick; static PageScrollStick page_scroll_stick; static vr::VROverlayHandle_t page_scroll_target=0; static std::chrono::steady_clock::time_point launcher_outside_at{}; static bool launcher_selected=false; static uint64_t launcher_session=0; static std::atomic launcher_prioritize_ui{true}; static std::atomic launcher_priority_active{false}; static std::string launcher_previous_dashboard; static std::set dashboard_keys; static bool launcher_restore_previous=true,launcher_previous_menu=false; static std::chrono::steady_clock::time_point launcher_keyboard_back_until{}; static EntryHold entry_hold; static uint64_t entry_source_buttons=0; static std::map entry_previous_buttons; static bool entry_actions_ready=false; static vr::VRActionSetHandle_t entry_set=0,gaze_set=0; static vr::VRActionHandle_t primary_action=0,upper_action=0,aim_action=0,close_action=0,navigate_action=0; static vr::VRInputValueHandle_t hands[2]{}; static vr::VRActionHandle_t gaze_action=0,confirm_action=0,gaze_haptic_action=0; static LauncherGazePress gaze_press; static bool gaze_active=false,launcher_calibrating=false; static json gaze_correction=nullptr,gaze_preview=nullptr; static std::string gaze_target_id; static float gaze_target_u=.5f,gaze_target_v=.5f; static vr::HmdVector3_t gaze_target_world{}; static bool gaze_target_cached=false; static bool gaze_matrix(const json& config,double (&m)[6]){ if(!config.is_object()||config.value("space",std::string{})!="headAngles"||!config.contains("matrix")||!config["matrix"].is_array()||config["matrix"].size()!=6)return false; for(int i=0;i<6;i++){if(!config["matrix"][i].is_number())return false;m[i]=config["matrix"][i].get();if(!std::isfinite(m[i])||std::abs(m[i])>4)return false;} double det=m[0]*m[4]-m[1]*m[3];return det>=.25&&det<=4&&std::abs(m[2])<=.5&&std::abs(m[5])<=.5; } static int gaze_feedback_hand=1; static std::chrono::steady_clock::time_point gaze_sent{},gaze_feedback_at{}; static EntryTrigger entry_buttons[2][2]; static EntryAnalogTrigger entry_analog[2]; static EntryPointer entry_pointer; static bool menu_open=false,keyboard_open=false,keyboard_close_pending=false; static float notification_curve_radius=1.3f; static bool keyboard_toolbar=false,keyboard_chars=false,keyboard_done=false; static MenuButtonInput menu_button; static cef_frame_t* keyboard_frame=nullptr; static std::string keyboard_view; static KeyboardMenuGuard keyboard_menu; static uint64_t keyboard_token=0;static vr::VROverlayHandle_t keyboard_target=0; static std::mutex queue_mutex; static std::mutex visibility_mutex; static json visible_views=nullptr; static std::deque queue; template struct Handler { T api{};std::atomic refs{1}; Handler(){api.base.size=sizeof(T);api.base.add_ref=[](cef_base_ref_counted_t* p){++reinterpret_cast(p)->refs;};api.base.release=[](cef_base_ref_counted_t* p){--reinterpret_cast(p)->refs;return 0;};api.base.has_one_ref=[](cef_base_ref_counted_t* p){return int(reinterpret_cast(p)->refs==1);};api.base.has_at_least_one_ref=[](cef_base_ref_counted_t* p){return int(reinterpret_cast(p)->refs>0);};} T* acquire(){api.base.add_ref(&api.base);return &api;} }; struct View { std::string key,plugin,title;int width=600,height=840;float render_scale=1.f;bool dashboard=false,shown=false,dirty=false,closing=false,closed=false; OverlayPlacement placement;NotificationClick notification_click;LauncherPointer launcher_pointer;std::chrono::steady_clock::time_point haptic_at{},activation_at{}; cef_mouse_event_t pointer{};bool pointer_valid=false,stick_scrolling=false;vr::TrackedDeviceIndex_t pointer_device=vr::k_unTrackedDeviceIndexInvalid;std::chrono::steady_clock::time_point pointer_at{}; cef_file_dialog_callback_t* file_dialog=nullptr;std::string file_dialog_id;bool file_multiple=false,file_directory=false; bool active_seen=false,notification_content=false;int browser_id=0;cef_browser_t* browser=nullptr;cef_browser_host_t* host=nullptr; vr::VROverlayHandle_t overlay=0,thumbnail=0;GLuint textures[2]{};int texture_slot=0;bool texture_ready[2]{};PaintDamage texture_damage[2];std::vector pixels; }; static std::map> views; static View* find_browser(cef_browser_t* b){int id=b->get_identifier(b);for(auto& [key,v]:views)if(v->browser_id==id)return v.get();return nullptr;} static Handler client; static Handler render_handler; static Handler dialog_handler; static unsigned long long file_dialog_sequence=0; static void cancel_file_dialog(View& v){if(!v.file_dialog)return;auto* callback=v.file_dialog;v.file_dialog=nullptr;v.file_dialog_id.clear();callback->cancel(callback);callback->base.release(&callback->base);} static Handler life_handler; static Handler display_handler; static Handler request_handler; static Handler load_handler; static void str(cef_string_t& o,const std::string& s){cef_string_utf8_to_utf16(s.data(),s.size(),&o);} static std::string text(const cef_string_t* s){cef_string_utf8_t o{};cef_string_utf16_to_utf8(s->str,s->length,&o);std::string r(o.str,o.length);cef_string_utf8_clear(&o);return r;} static void execute(cef_frame_t* f,const std::string& code){if(!f||!f->is_valid(f))return;cef_string_t script{},url{};str(script,code);f->execute_java_script(f,&script,&url,1);cef_string_utf16_clear(&script);} // Steam Frame's BarSurface/BarTab dimensions and state colors (80px, radius 6px). static std::vector icon(bool hover,bool active){ std::vector p(128*128*4); auto rounded=[](float x,float y,float half,float radius){float a=std::abs(x)-half+radius,b=std::abs(y)-half+radius;return std::hypot(std::max(a,0.f),std::max(b,0.f))+std::min(std::max(a,b),0.f)-radius;}; for(int y=0;y<128;y++)for(int x=0;x<128;x++){ float coverage=std::clamp(.5f-rounded(x-63.5f,y-63.5f,64.f,9.6f),0.f,1.f); int gx=std::min(47,int((x+.5f)*48/128)),gy=std::min(47,int((y+.5f)*48/128)); float mark=framely_brand::mask[gy*48+gx]/255.f; int i=(y*128+x)*4; const int normal[3]={23,25,28},highlight[3]={32,35,40},blue[3]={147,197,237}; for(int c=0;c<3;c++){float bg=(hover||active)?highlight[c]:normal[c];p[i+c]=uint8_t(bg+(blue[c]-bg)*mark);} // 32px-wide, 6px-high tab indicator is clipped to its upper 3px by the BarSurface. if(active){float qx=std::abs(x-63.5f)-20.8f,qy=std::abs(y-129.6f);float line=std::clamp(5.3f-std::hypot(std::max(qx,0.f),qy),0.f,1.f);const int blue[3]={26,159,255};for(int c=0;c<3;c++)p[i+c]=uint8_t(p[i+c]+(blue[c]-p[i+c])*line);} p[i+3]=uint8_t(255*coverage); } return p; } static json poll_http(int port){int fd=socket(AF_INET,SOCK_STREAM,0);if(fd<0)throw std::runtime_error("HTTP socket failed");timeval timeout{1,0};setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));sockaddr_in addr{};addr.sin_family=AF_INET;addr.sin_port=htons(port);inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr);if(connect(fd,reinterpret_cast(&addr),sizeof(addr))){close(fd);throw std::runtime_error("UI agent unavailable");}std::string payload;{std::lock_guard lock(visibility_mutex);payload=visible_views.dump();}std::string req="POST /host/poll HTTP/1.1\r\nHost: 127.0.0.1:"+std::to_string(port)+"\r\nX-Framely-Native: "+token+"\r\nContent-Type: application/json\r\nContent-Length: "+std::to_string(payload.size())+"\r\nConnection: close\r\n\r\n"+payload;size_t sent=0;while(sent0){response.append(buffer,n);if(response.size()>4*1024*1024){close(fd);throw std::runtime_error("HTTP body limit");}}close(fd);auto body=response.find("\r\n\r\n");if(body==std::string::npos||response.find("200 OK")==std::string::npos)throw std::runtime_error("Invalid agent response");return json::parse(response.substr(body+4));} // Frame renders the native laser cursor in the default overlay sort group. // Raising a panel above that group can obscure the runtime cursor. // OpenVR owns and renders the laser cursor. Coordinates must remain in the // runtime's overlay mouse space, before conversion to CEF's top-left origin. // Keep a wider curved mesh around the narrow button; transparent margins never hit input. static constexpr int dock_canvas_width=640,dock_canvas_height=128,dock_icon_left=256; static std::vector dock_icon(bool hover,bool active,float progress=0){ auto source=icon(hover,active);entry_progress_border(source,progress);notification_badge(source,notification_badge_count.load());std::vector canvas(dock_canvas_width*dock_canvas_height*4,0); for(int row=0;row upload; void init(){ glGenTextures(2,textures); for(auto texture:textures){ glBindTexture(GL_TEXTURE_2D,texture); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,dock_canvas_width,dock_canvas_height,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr); } } bool submit(vr::VROverlayHandle_t overlay,const std::vector& pixels){ glXMakeContextCurrent(xdisplay,glsurface,glsurface,glcontext); 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); // 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 "<IsActiveDashboardOverlay(selected))return selected; refreshed=now;selected=0; overlays->FindOverlay("valve.steam.gamepadui.main",&selected); return selected; } static void hover_haptic(vr::VROverlayHandle_t overlay,std::chrono::steady_clock::time_point& last,bool activation=false){ auto now=std::chrono::steady_clock::now();if(now-lastIsOverlayVisible(overlay)||!overlays->IsHoverTargetOverlay(overlay))return; last=now;if(ui_sounds)ui_audio.play(activation); if(ui_haptics&&!activation){auto e=overlays->TriggerLaserMouseHapticVibration(overlay,.012f,120.f,.15f);if(e)std::cerr<<"Hover haptic failed "<SetOverlayCursorPositionOverride(overlay,&position);} else if(event.eventType==vr::VREvent_FocusLeave)overlays->ClearOverlayCursorPositionOverride(overlay); } static void update_menu_modal(){ if(!views.count("menu"))return; auto& menu=*views.at("menu"); bool toast=views.count("notifications")&&views.at("notifications")->shown&&!views.at("notifications")->closing; // A modal quick menu must not monopolize the laser while a toast needs input. overlays->SetOverlayFlag(menu.overlay,vr::VROverlayFlags_WantsModalBehavior,menu.shown&&!toast&&!(keyboard_open&&keyboard_view=="menu")); } static void show(View& v,bool shown){ if(v.closing)return; bool actual=overlays->IsOverlayVisible(v.overlay); if(v.key=="notifications"&&(shown!=v.shown||shown!=actual)){ auto e=overlays->SetOverlayInputMethod(v.overlay,shown?vr::VROverlayInputMethod_Mouse:vr::VROverlayInputMethod_None); if(!e)e=overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,shown); if(e){std::cerr<<"Notification input state failed "<SetOverlayInputMethod(v.overlay,vr::VROverlayInputMethod_None);} } if(shown!=actual){auto e=shown?overlays->ShowOverlay(v.overlay):overlays->HideOverlay(v.overlay);if(e){std::cerr<<"Overlay visibility failed "<ClearOverlayCursorPositionOverride(v.overlay);if(v.key=="notifications"&&v.notification_click.active){cef_mouse_event_t outside{};outside.x=outside.y=-1;v.host->send_mouse_click_event(v.host,&outside,MBT_LEFT,1,1);v.notification_click.clear();}} if(v.key!="notifications")v.host->was_hidden(v.host,!shown); v.shown=shown;if(shown)v.host->invalidate(v.host,PET_VIEW); if(v.key=="menu"||v.key=="notifications")update_menu_modal(); } } static void keyboard_state(bool open){if(keyboard_frame)execute(keyboard_frame,std::string("window.dispatchEvent(new CustomEvent('framely.keyboard',{detail:")+(open?"true":"false")+"}))");} static void close_keyboard(bool hide=true){keyboard_state(false);if(keyboard_open&&hide)overlays->HideKeyboard();keyboard_open=false;keyboard_close_pending=false;keyboard_toolbar=false;keyboard_chars=false;keyboard_done=false;keyboard_target=0;keyboard_menu.end(std::chrono::steady_clock::now());if(keyboard_view=="menu")update_menu_modal();if(keyboard_frame){keyboard_frame->base.release(&keyboard_frame->base);keyboard_frame=nullptr;}keyboard_view.clear();} static bool keyboard_event(const vr::VREvent_t& e,vr::VROverlayHandle_t source_overlay=0){ // The runtime toolbar opens the keyboard without a host.keyboard request. if(e.eventType==vr::VREvent_KeyboardOpened_Global){ auto target=e.data.keyboard.overlayHandle; if(keyboard_open&&target==keyboard_target)return true; View* owner=nullptr;for(auto&[key,v]:views)if(v->overlay==target&&!v->closing){owner=v.get();break;} if(!owner)return false; close_keyboard(false);owner->host->set_focus(owner->host,1); keyboard_frame=owner->browser->get_focused_frame(owner->browser);keyboard_view=owner->key; keyboard_target=target;keyboard_token=e.data.keyboard.uUserValue;keyboard_open=true;keyboard_toolbar=true; keyboard_menu.begin(owner->key=="menu");keyboard_state(true);return true; } if(e.eventType!=vr::VREvent_KeyboardCharInput&&e.eventType!=vr::VREvent_KeyboardDone&&e.eventType!=vr::VREvent_KeyboardClosed&&e.eventType!=vr::VREvent_KeyboardClosed_Global)return false; if(!keyboard_open)return true; if(e.data.keyboard.uUserValue&&e.data.keyboard.uUserValue!=keyboard_token)return true; if(e.data.keyboard.overlayHandle&&e.data.keyboard.overlayHandle!=keyboard_target)return true; if(e.eventType==vr::VREvent_KeyboardCharInput){ // OpenVR duplicates character events globally. Only the owner Overlay edits. if(keyboard_edit_source(source_overlay,keyboard_target)){keyboard_chars=true;keyboard_edit(keyboard_frame,std::string(e.data.keyboard.cNewInput,strnlen(e.data.keyboard.cNewInput,sizeof(e.data.keyboard.cNewInput))));} }else {keyboard_done=keyboard_done||e.eventType==vr::VREvent_KeyboardDone;keyboard_close_pending=true;} return true; } static void close_view(View& v){if(v.closing)return;cancel_file_dialog(v);if(keyboard_view==v.key)close_keyboard();v.closing=true;if(v.key=="menu")overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_WantsModalBehavior,false);overlays->ClearOverlayCursorPositionOverride(v.overlay);overlays->HideOverlay(v.overlay);v.host->close_browser(v.host,1);} static void anchor_to_head(View& v,float width,float x=0,float y=0,float z=-1.35f){vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]{};vr_system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding,0,poses,vr::k_unMaxTrackedDeviceCount);if(!poses[0].bPoseIsValid)return;auto m=poses[0].mDeviceToAbsoluteTracking;for(int r=0;r<3;r++)m.m[r][3]+=m.m[r][0]*x+m.m[r][1]*y+m.m[r][2]*z;overlays->SetOverlayWidthInMeters(v.overlay,width);overlays->SetOverlayTransformAbsolute(v.overlay,vr::TrackingUniverseStanding,&m);} static void anchor_notification(View& v){ // Let the compositor follow the HMD's render pose; a captured standing-space // transform leaves the toast behind when the user turns or moves their head. vr::HmdMatrix34_t head{{{1,0,0,0},{0,1,0,0},{0,0,1,0}}}; NotificationPlacement toast{}; if(!notification_placement(head,notification_curve_radius,toast))return; auto error=overlays->SetOverlayWidthInMeters(v.overlay,toast.width); if(!error)error=overlays->SetOverlayTransformTrackedDeviceRelative(v.overlay,vr::k_unTrackedDeviceIndex_Hmd,&toast.transform); if(error){std::cerr<<"Notification head attachment failed "<ShowDashboard(key.c_str());else if(key!="notifications"&&key!="launcher")show(v,true);return v;} // Use 1.5x for launcher artwork and 2x for scrolling panels. CEF readback, // texture upload and launcher mipmap generation scale with the pixel count. if(views.size()>=34)throw std::runtime_error("Window limit exceeded");auto v=std::make_unique();v->key=key;v->plugin=plugin;v->title=title;v->dashboard=dock;v->width=width;v->height=height;v->render_scale=key=="launcher"?1.5f:(key=="framely.manager"||key=="menu"||key=="notifications")?2:1; auto error=dock?overlays->CreateDashboardOverlay(key.c_str(),title.c_str(),&v->overlay,&v->thumbnail):overlays->CreateOverlay(key.c_str(),title.c_str(),&v->overlay);if(error)throw std::runtime_error("Overlay creation failed: "+std::to_string(error)); overlays->SetOverlayInputMethod(v->overlay,key=="notifications"?vr::VROverlayInputMethod_None:vr::VROverlayInputMethod_Mouse);vr::HmdVector2_t scale{{float(width),float(height)}};overlays->SetOverlayMouseScale(v->overlay,&scale);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_HideLaserIntersection,false);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_SendVRSmoothScrollEvents,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_VisibleInDashboard,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_SortWithNonSceneOverlays,false);overlays->SetOverlaySortOrder(v->overlay,0); // Keep the runtime's full dashboard controls, including its keyboard and close buttons. if(dock){ if(key=="launcher")overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_NoDashboardTab,true); auto result=overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_MinimalControlBar,key=="launcher"); if(result)std::cerr<<"Dashboard controls unavailable "<SetOverlayFlag(v->overlay,flag,true); if(result)std::cerr<<"Dashboard control unavailable "<SetOverlayRaw(v->thumbnail,pixels.data(),128,128,4);overlays->SetOverlayWidthInMeters(v->overlay,physical_width>0?physical_width:(key=="framely.manager"?3.2f:key=="launcher"?3.6f:1.2f));}else if(key!="menu"&&key!="notifications"&&key!="launcher"){anchor_to_head(*v,physical_width>0?physical_width:1.0f);} cef_window_info_t info{};info.size=sizeof(info);info.windowless_rendering_enabled=1;cef_browser_settings_t bs{};bs.size=sizeof(bs);bs.windowless_frame_rate=browser_frame_rate();bs.background_color=0;std::cout<<"Browser frame rate "<DestroyOverlay(v->overlay);if(v->thumbnail)overlays->DestroyOverlay(v->thumbnail);throw std::runtime_error("Browser creation failed");}v->browser=browser;v->browser_id=browser->get_identifier(browser);v->host=browser->get_host(browser);glGenTextures(2,v->textures);auto* ptr=v.get();views.emplace(key,std::move(v));ptr->host->notify_screen_info_changed(ptr->host);ptr->host->was_resized(ptr->host);ptr->host->was_hidden(ptr->host,key!="notifications");if(dock&&key!="launcher")overlays->ShowDashboard(key.c_str());else if(key!="menu"&&key!="notifications"&&key!="launcher")show(*ptr,true);return *ptr; } static void plugin_thumbnail(View& view,const std::string& encoded){ if(!view.thumbnail||encoded.empty()||encoded.size()>1400000)return; cef_string_t value{};str(value,encoded);auto* data=cef_base64_decode(&value);cef_string_utf16_clear(&value);if(!data)return; std::vector png(data->get_size(data));data->get_data(data,png.data(),png.size(),0);data->base.release(&data->base); auto* image=cef_image_create();if(!image)return; if(!image->add_png(image,1,png.data(),png.size())||image->get_width(image)>1024||image->get_height(image)>1024){image->base.release(&image->base);return;} int width=0,height=0;auto* bitmap=image->get_as_bitmap(image,1,CEF_COLOR_TYPE_RGBA_8888,CEF_ALPHA_TYPE_POSTMULTIPLIED,&width,&height);image->base.release(&image->base); if(!bitmap)return;if(width<=0||height<=0||width>1024||height>1024){bitmap->base.release(&bitmap->base);return;} std::vector source(bitmap->get_size(bitmap));bitmap->get_data(bitmap,source.data(),source.size(),0);bitmap->base.release(&bitmap->base); if(source.size()!=size_t(width*height*4))return; std::vector pixels(128*128*4,0);float scale=std::min(112.f/width,112.f/height);int w=std::max(1,int(width*scale)),h=std::max(1,int(height*scale)); for(int y=0;ySetOverlayRaw(view.thumbnail,pixels.data(),128,128,4); } static void navigate(View& view,const std::string& hash){auto* frame=view.browser->get_main_frame(view.browser);execute(frame,"location.hash="+json(hash).dump()+";window.dispatchEvent(new Event('hashchange'))");frame->base.release(&frame->base);} static bool launcher_dock_interaction(){ // These are Steam's Dock bar and native window controls on Frame, not app // content. The runtime keeps ownership of their positioning and interaction. bool hovered=false; for(const char* key:{"valve.steam.gamepadui.bar","valve.steam.gamepadui.floatingfooter","system.systemui"}){ vr::VROverlayHandle_t overlay=0; if(!overlays->FindOverlay(key,&overlay)&&overlays->IsOverlayVisible(overlay)&&overlays->IsHoverTargetOverlay(overlay))hovered=true; } bool held=false;for(auto& hand:entry_buttons)for(auto& trigger:hand)held|=trigger.held; auto now=std::chrono::steady_clock::now();launcher_dock.observe(hovered,held,now); if(launcher_dock.holds(now)){launcher_outside_at={};return true;} return false; } static std::string selected_dashboard(){ std::set keys=dashboard_keys;keys.insert("valve.steam.gamepadui.main");keys.insert("system.systemui"); for(auto& [key,v]:views)if(v->dashboard&&!v->closing)keys.insert(key); for(auto& key:keys){if(key=="launcher")continue;vr::VROverlayHandle_t handle=0;if(!overlays->FindOverlay(key.c_str(),&handle)&&overlays->IsActiveDashboardOverlay(handle))return key;} return {}; } static void observe_dashboard(const vr::VREvent_t& event){ if(event.eventType!=vr::VREvent_OverlayCreated&&event.eventType!=vr::VREvent_OverlayShown&&event.eventType!=vr::VREvent_DashboardOverlayCreated)return; char key[1024]{};vr::EVROverlayError error{};overlays->GetOverlayKey(event.data.overlay.overlayHandle,key,sizeof(key),&error); if(!error&&key[0]&&dashboard_keys.size()<512)dashboard_keys.insert(key); } static void finish_launcher_close(){ launcher_fade.reset(); if(!views.count("launcher"))return;auto& v=*views.at("launcher"); overlays->SetOverlayAlpha(v.overlay,0.f); // Do not steal focus if Steam or the user already selected another window. if(!overlays->IsActiveDashboardOverlay(v.overlay)){overlays->HideOverlay(v.overlay);return;} if(launcher_restore_previous&&!launcher_previous_dashboard.empty()){ vr::VROverlayHandle_t previous=0;if(!overlays->FindOverlay(launcher_previous_dashboard.c_str(),&previous)){ overlays->ShowDashboard(launcher_previous_dashboard.c_str());menu_open=launcher_previous_menu;return; } } // Unknown/destroyed previous windows are left to Steam's window selection. overlays->HideOverlay(v.overlay); if(!launcher_restore_previous)overlays->ShowDashboard("valve.steam.gamepadui.main"); if(launcher_restore_previous)menu_open=launcher_previous_menu; } static void hide_launcher(bool restore=true,bool immediate=false){ if(keyboard_view=="launcher")close_keyboard(); if(launcher_calibrating&&views.count("launcher")){auto* frame=views.at("launcher")->browser->get_main_frame(views.at("launcher")->browser);execute(frame,"location.hash='';window.dispatchEvent(new Event('hashchange'))");frame->base.release(&frame->base);} const bool was_open=launcher_open;if(was_open)launcher_restore_previous=restore;launcher_calibrating=false;launcher_open=false;launcher_selected=false;launcher_dock.clear();launcher_stick={}; if(immediate){launcher_close_at={};finish_launcher_close();return;} if(was_open&&views.count("launcher")&&overlays->IsActiveDashboardOverlay(views.at("launcher")->overlay)){ auto* frame=views.at("launcher")->browser->get_main_frame(views.at("launcher")->browser); execute(frame,"window.dispatchEvent(new Event('framely.launcher.closing'))");frame->base.release(&frame->base); auto now=std::chrono::steady_clock::now();launcher_fade.close(now);launcher_close_at=now+std::chrono::milliseconds(launcher_fade.reduced?0:240); } } static void launcher_back(){ auto now=std::chrono::steady_clock::now();if(nowget_main_frame(view.browser); execute(frame,"window.dispatchEvent(new Event('framely.back'))");frame->base.release(&frame->base); } static bool launcher_accepts(const vr::VREvent_t& e){ bool held=false;for(auto& hand:entry_buttons)for(auto& button:hand)held|=button.held; return launcher_dismiss.allows(std::chrono::steady_clock::now(),held,e.eventAgeSeconds); } static void toggle_entry(bool launcher){ if(launcher){ if(launcher_open){hide_launcher();return;} if(!launcher_close_at.time_since_epoch().count()||!views.count("launcher")||!overlays->IsActiveDashboardOverlay(views.at("launcher")->overlay)){launcher_previous_dashboard=selected_dashboard();launcher_previous_menu=menu_open;}launcher_restore_previous=true; menu_open=false;if(views.count("menu"))show(*views.at("menu"),false); auto& view=create_view("launcher","Framely launcher",origin+"/launcher",true,1440,800); vr::VREvent_t stale{};while(overlays->PollNextOverlayEvent(view.overlay,&stale,sizeof(stale))){} ++launcher_session;launcher_close_at={};launcher_dock.clear();launcher_dismiss.opened(std::chrono::steady_clock::now());launcher_open=true;launcher_selected=false;launcher_stick={};launcher_outside_at={}; // Wake CEF before activation. A small nonzero alpha keeps Steam activation // working without exposing the previous session's full-opacity texture. launcher_fade.open(std::chrono::steady_clock::now()); view.host->was_hidden(view.host,0);view.host->invalidate(view.host,PET_VIEW); overlays->SetOverlayAlpha(view.overlay,.02f);overlays->ShowOverlay(view.overlay); overlays->ShowDashboard("launcher"); auto* frame=view.browser->get_main_frame(view.browser);execute(frame,"window.framelyLauncherSession="+std::to_string(launcher_session)+";window.dispatchEvent(new Event('framely.launcher.open'))");frame->base.release(&frame->base); }else{hide_launcher(false);menu_open=!menu_open;if(menu_open)menu_button.opened(std::chrono::steady_clock::now());} } static void setup_entry_actions(){ char exe[4096]{};auto n=readlink("/proc/self/exe",exe,sizeof(exe)-1);if(n<=0)return; auto base=std::filesystem::path(exe).parent_path();auto manifest=base/"../../share/input/actions.json"; if(!std::filesystem::exists(manifest))manifest=base/"../../native/input/actions.json"; auto* input=vr::VRInput();if(!input||!std::filesystem::exists(manifest))return; auto error=input->SetActionManifestPath(std::filesystem::absolute(manifest).lexically_normal().c_str());if(error!=vr::VRInputError_None){std::cerr<<"Action manifest failed: "<GetActionSetHandle("/actions/entry",&entry_set)==vr::VRInputError_None&&input->GetActionHandle("/actions/entry/in/primary",&primary_action)==vr::VRInputError_None&&input->GetActionHandle("/actions/entry/in/upper",&upper_action)==vr::VRInputError_None&&input->GetActionHandle("/actions/entry/in/aim",&aim_action)==vr::VRInputError_None; input->GetActionHandle("/actions/entry/in/close",&close_action);input->GetActionHandle("/actions/entry/in/navigate",&navigate_action); input->GetActionSetHandle("/actions/gaze",&gaze_set);input->GetActionHandle("/actions/gaze/in/eye",&gaze_action);input->GetActionHandle("/actions/entry/in/confirm",&confirm_action);input->GetActionHandle("/actions/entry/out/gazeHaptic",&gaze_haptic_action); input->GetInputSourceHandle("/user/hand/left",&hands[0]);input->GetInputSourceHandle("/user/hand/right",&hands[1]); std::cout<<"Framely independent trigger bindings "<<(entry_actions_ready?"ready":"unavailable")<pointer_valid||!capture_view_visible(v->closing,overlays->IsOverlayVisible(v->overlay),v->dashboard,overlays->IsDashboardVisible(),overlays->IsActiveDashboardOverlay(v->overlay))||!overlays->IsHoverTargetOverlay(v->overlay))continue; if(!target||(v->pointer_device==device&&target->pointer_device!=device)||(v->pointer_device==target->pointer_device&&v->pointer_at>target->pointer_at))target=v.get(); } if(!target){page_scroll_stick.clear();page_scroll_target=0;return;} if(page_scroll_target!=target->overlay){page_scroll_stick.clear();page_scroll_target=target->overlay;} target->stick_scrolling=active&&std::isfinite(x)&&std::isfinite(y)&&std::max(std::abs(x),std::abs(y))>.18f; auto delta=page_scroll_stick.update(x,y,target->stick_scrolling,now); if(delta.x||delta.y){auto mouse=target->pointer;mouse.modifiers=0;target->host->send_mouse_wheel_event(target->host,&mouse,delta.x,delta.y);} } static bool launcher_gaze_allowed(){ if(!launcher_open||keyboard_open||!views.count("launcher")||launcher_dock_interaction())return false; auto& v=*views.at("launcher");return !v.closing&&overlays->IsActiveDashboardOverlay(v.overlay)&&overlays->IsOverlayVisible(v.overlay)&&(launcher_calibrating||(!overlays->IsHoverTargetOverlay(v.overlay)&&!v.launcher_pointer.active)); } static void launcher_gaze_event(const json& sample){ if(!views.count("launcher"))return;auto& v=*views.at("launcher");if(v.closing||!v.browser)return; auto* frame=v.browser->get_main_frame(v.browser);execute(frame,"window.dispatchEvent(new CustomEvent('framely.launcher.gaze',{detail:"+sample.dump()+"}))");frame->base.release(&frame->base); } static void launcher_gaze(vr::IVRInput* input,bool allowed,const bool (&connected)[2],const bool (&buttons)[2],std::chrono::steady_clock::time_point now){ allowed=allowed&&launcher_gaze_allowed(); vr::VREyeTrackingData_t eye{};vr::VROverlayIntersectionParams_t ray{};vr::VROverlayIntersectionResults_t hit{}; auto sampled_at=std::chrono::steady_clock::now(); bool tracked=allowed&&gaze_action&&input->GetEyeTrackingDataRelativeToNow(gaze_action,vr::TrackingUniverseStanding,0,&eye,sizeof(eye))==vr::VRInputError_None&&launcher_gaze_ray(eye,ray); vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]{}; // Use the same nominal time for gaze and head pose, rather than pairing gaze // with a later/predicted rendering pose while the user turns their head. if(tracked)vr_system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding,std::chrono::duration(sampled_at-std::chrono::steady_clock::now()).count(),poses,vr::k_unMaxTrackedDeviceCount); GazeAngles raw{},expected{};tracked=tracked&&poses[0].bPoseIsValid&&gaze_head_angles(poses[0].mDeviceToAbsoluteTracking,ray.vDirection,raw); bool target_valid=false; if(tracked&&launcher_calibrating&&!gaze_target_id.empty()){ auto& view=*views.at("launcher");auto& head=poses[0].mDeviceToAbsoluteTracking; auto sample=[&](GazeAngles angles,float& u,float& v){auto candidate=gaze_world_ray(head,ray.vSource,angles);vr::VROverlayIntersectionResults_t result{};bool valid=overlays->ComputeOverlayIntersection(view.overlay,&candidate,&result);u=result.vUVs.v[0];v=result.vUVs.v[1];return valid&&std::isfinite(u)&&std::isfinite(v);}; if(gaze_target_cached&&gaze_angles_to_point(head,ray.vSource,gaze_target_world,expected)){float u,v;target_valid=sample(expected,u,v)&&std::hypot(u-gaze_target_u,v-gaze_target_v)<.001f;} if(!target_valid){ vr::HmdMatrix34_t seed_pose{};GazeAngles seed{}; if(!overlays->GetTransformForOverlayCoordinates(view.overlay,vr::TrackingUniverseStanding,{{gaze_target_u*view.width,gaze_target_v*view.height}},&seed_pose)){ vr::HmdVector3_t seed_point{{seed_pose.m[0][3],seed_pose.m[1][3],seed_pose.m[2][3]}}; bool solved=gaze_angles_to_point(head,ray.vSource,seed_point,seed)&&gaze_target_inverse(seed,gaze_target_u,gaze_target_v,sample,expected); if(!solved){vr::HmdMatrix34_t center{};if(!overlays->GetTransformForOverlayCoordinates(view.overlay,vr::TrackingUniverseStanding,{{view.width*.5f,view.height*.5f}},¢er)){vr::HmdVector3_t point{{center.m[0][3],center.m[1][3],center.m[2][3]}};solved=gaze_angles_to_point(head,ray.vSource,point,seed)&&gaze_target_inverse(seed,gaze_target_u,gaze_target_v,sample,expected);}} if(solved){ auto candidate=gaze_world_ray(head,ray.vSource,expected);vr::VROverlayIntersectionResults_t result{}; target_valid=overlays->ComputeOverlayIntersection(view.overlay,&candidate,&result);if(target_valid){gaze_target_world=result.vPoint;gaze_target_cached=true;} } } } } if(tracked){double matrix[6];if(gaze_matrix(launcher_calibrating?gaze_preview:gaze_correction,matrix))ray=gaze_world_ray(poses[0].mDeviceToAbsoluteTracking,ray.vSource,gaze_correct(raw,matrix));} bool valid=tracked&&overlays->ComputeOverlayIntersection(views.at("launcher")->overlay,&ray,&hit)&&std::isfinite(hit.vUVs.v[0])&&std::isfinite(hit.vUVs.v[1])&&hit.vUVs.v[0]>=0&&hit.vUVs.v[0]<=1&&hit.vUVs.v[1]>=0&&hit.vUVs.v[1]<=1; // Looking away during a press keeps the locked target. Tracking loss cancels. auto edge=gaze_press.update(!launcher_calibrating&&tracked&&(valid||gaze_press.held),connected,buttons); if(edge==LauncherGazePress::Down)gaze_feedback_hand=gaze_press.owner; if(edge==LauncherGazePress::Cancel||(!tracked&&gaze_active)){launcher_gaze_event(!allowed||edge==LauncherGazePress::Cancel?json{{"valid",false},{"phase","cancel"}}:json{{"valid",false}});gaze_active=false;return;} if(!tracked)return; if(edge==LauncherGazePress::Down||edge==LauncherGazePress::Up){launcher_gaze_event({{"valid",true},{"phase",edge==LauncherGazePress::Down?"down":"up"}});gaze_sent=now;} else if(now-gaze_sent>=std::chrono::milliseconds(16)){ auto& v=*views.at("launcher");json event=valid?json{{"valid",true},{"x",hit.vUVs.v[0]*v.width},{"y",(1-hit.vUVs.v[1])*v.height}}:json{{"valid",bool(gaze_press.held)}}; if(launcher_calibrating){event["valid"]=tracked&&target_valid;event["targetId"]=gaze_target_id;event["targetUnavailable"]=tracked&&!target_valid;event["rawAngles"]={raw.yaw,raw.pitch};event["targetAngles"]={expected.yaw,expected.pitch};event["headPose"]=json::array();for(int r=0;r<3;r++)for(int c=0;c<4;c++)event["headPose"].push_back(poses[0].mDeviceToAbsoluteTracking.m[r][c]);} launcher_gaze_event(event);gaze_sent=now; } gaze_active=true; } static void entry_actions(vr::VROverlayHandle_t button,bool available){ if(!entry_actions_ready)return;auto* input=vr::VRInput();vr::VRActiveActionSet_t sets[2]{};sets[0].ulActionSet=entry_set;sets[1].ulActionSet=gaze_set; // Only the eye action participates in overlay priority. Controller bindings // keep their existing priority so Steam laser and Dock controls are preserved. if(launcher_gaze_allowed()&&!entry_hold.pressed&&!(available&&(entry_pointer.inside||overlays->IsHoverTargetOverlay(button))))sets[1].nPriority=vr::k_nActionSetOverlayGlobalPriorityMin; if(input->UpdateActionState(sets,sizeof(sets[0]),gaze_set?2:1)!=vr::VRInputError_None)return; auto now=std::chrono::steady_clock::now(); // Dock ownership must be checked before reading confirm edges. Gaze focus // alone does not own a trigger aimed at the entry, even before mouse-down // arrives. Disabling gaze disarms it until all confirm inputs are released. bool gaze_allowed=launcher_gaze_allowed()&&!entry_hold.pressed&&!(available&&(entry_pointer.inside||overlays->IsHoverTargetOverlay(button)));bool gaze_connected[2]{},gaze_buttons[2]{}; float stick_x=0,stick_y=0;bool stick_active=false;vr::TrackedDeviceIndex_t stick_device=vr::k_unTrackedDeviceIndexInvalid; for(int hand=0;hand<2;hand++){ vr::InputPoseActionData_t pose{};auto device=vr_system->GetTrackedDeviceIndexForControllerRole(hand?vr::TrackedControllerRole_RightHand:vr::TrackedControllerRole_LeftHand);bool hit=available&&entry_pointer.hit(device); bool pose_ok=available&&input->GetPoseActionDataRelativeToNow(aim_action,vr::TrackingUniverseStanding,0,&pose,sizeof(pose),hands[hand])==vr::VRInputError_None&&pose.bActive&&pose.pose.bPoseIsValid; if(available&&!pose_ok&&device!=vr::k_unTrackedDeviceIndexInvalid){ vr::TrackedDevicePose_t tracked[vr::k_unMaxTrackedDeviceCount]{};vr_system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding,0,tracked,vr::k_unMaxTrackedDeviceCount); char model[256]{};vr_system->GetStringTrackedDeviceProperty(device,vr::Prop_RenderModelName_String,model,sizeof(model)); vr::RenderModel_ControllerMode_State_t mode{};vr::RenderModel_ComponentState_t tip{}; if(tracked[device].bDeviceIsConnected&&tracked[device].bPoseIsValid&&vr::VRRenderModels()->GetComponentStateForDevicePath(model,"tip",hands[hand],&mode,&tip)){ pose.pose.mDeviceToAbsoluteTracking=entry_tip_pose(tracked[device].mDeviceToAbsoluteTracking,tip.mTrackingToComponentLocal);pose_ok=true; } } if(pose_ok){auto& m=pose.pose.mDeviceToAbsoluteTracking;vr::VROverlayIntersectionParams_t params{};params.eOrigin=vr::TrackingUniverseStanding;for(int r=0;r<3;r++){params.vSource.v[r]=m.m[r][3];params.vDirection.v[r]=-m.m[r][2];}vr::VROverlayIntersectionResults_t result{};hit=hit||(overlays->IsHoverTargetOverlay(button)&&overlays->ComputeOverlayIntersection(button,¶ms,&result)&&result.vUVs.v[0]>=float(dock_icon_left)/dock_canvas_width&&result.vUVs.v[0]<=float(dock_icon_left+128)/dock_canvas_width);} if(hit)gaze_allowed=false; gaze_connected[hand]=device!=vr::k_unTrackedDeviceIndexInvalid&&vr_system->IsTrackedDeviceConnected(device); if(gaze_connected[hand]){vr::InputDigitalActionData_t confirm{};bool active=input->GetDigitalActionData(confirm_action,&confirm,sizeof(confirm),hands[hand])==vr::VRInputError_None&&confirm.bActive;gaze_buttons[hand]=active&&confirm.bState; if(!active){char model[256]{};vr_system->GetStringTrackedDeviceProperty(device,vr::Prop_RenderModelName_String,model,sizeof(model));vr::RenderModel_ControllerMode_State_t mode{};vr::RenderModel_ComponentState_t state{}; if(hand==1)gaze_buttons[hand]=vr::VRRenderModels()->GetComponentStateForDevicePath(model,"button_a",hands[hand],&mode,&state)&&(state.uProperties&vr::VRComponentProperty_IsPressed); else{vr::VRControllerState_t controller{};gaze_buttons[hand]=vr_system->GetControllerState(device,&controller,sizeof(controller))&&(controller.ulButtonPressed&(vr::ButtonMaskFromId(vr::k_EButton_DPad_Down)|vr::ButtonMaskFromId(vr::k_EButton_A)));} } } for(int kind=0;kind<2;kind++){vr::InputDigitalActionData_t data{};bool active=input->GetDigitalActionData(kind?upper_action:primary_action,&data,sizeof(data),hands[hand])==vr::VRInputError_None&&data.bActive; bool down=data.bState;if(kind==0&&active)entry_analog[hand].held=down; // Dashboard focus suppresses action data. Frame's render-model components // read the separate physical input paths, without changing Steam bindings // or the user's global input-override setting. if(!active&&device!=vr::k_unTrackedDeviceIndexInvalid&&vr_system->IsTrackedDeviceConnected(device)){ char type[256]{},model[256]{};vr_system->GetStringTrackedDeviceProperty(device,vr::Prop_ControllerType_String,type,sizeof(type)); if(std::string(type)=="frame_controller"){ vr_system->GetStringTrackedDeviceProperty(device,vr::Prop_RenderModelName_String,model,sizeof(model)); vr::RenderModel_ControllerMode_State_t mode{};vr::RenderModel_ComponentState_t state{}; active=vr::VRRenderModels()->GetComponentStateForDevicePath(model,kind?"bumper":"trigger",hands[hand],&mode,&state); down=active&&(state.uProperties&vr::VRComponentProperty_IsPressed); if(active&&kind==0){ // Frame's primary trigger is analog: its animated component has no // IsPressed flag. Calibrate its travel from the model's legacy axis 1, // then match Steam's 50% press / 45% release thresholds. vr::VRControllerState_t rest_input{},full_input{};full_input.rAxis[1].x=1; vr::RenderModel_ComponentState_t rest{},full{};float pull=0; if(vr::VRRenderModels()->GetComponentState(model,"trigger",&rest_input,&mode,&rest)&&vr::VRRenderModels()->GetComponentState(model,"trigger",&full_input,&mode,&full)&&entry_trigger_pull(rest,full,state,pull))down=entry_analog[hand].update(pull)||down; } } } if(kind==0&&!active)entry_analog[hand].held=false; if(kind==0&&active)gaze_buttons[hand]|=down; bool was_held=entry_buttons[hand][kind].held; entry_buttons[hand][kind].update(active,down,hit,now); if(!(gaze_allowed&&gaze_active)&&kind==0&&active&&down&&!was_held&&launcher_open&&!launcher_calibrating&&!hit&&!launcher_dock_interaction()&&launcher_dismiss.allows(now,true)&&views.count("launcher")&&!overlays->IsHoverTargetOverlay(views.at("launcher")->overlay))launcher_outside_at=now+std::chrono::milliseconds(60); } if(launcher_open&&hand==1){vr::InputDigitalActionData_t close{};bool active=input->GetDigitalActionData(close_action,&close,sizeof(close),hands[hand])==vr::VRInputError_None&&close.bActive;bool down=close.bState; if(!active&&device!=vr::k_unTrackedDeviceIndexInvalid&&vr_system->IsTrackedDeviceConnected(device)){char model[256]{};vr_system->GetStringTrackedDeviceProperty(device,vr::Prop_RenderModelName_String,model,sizeof(model));vr::RenderModel_ControllerMode_State_t mode{};vr::RenderModel_ComponentState_t state{};active=vr::VRRenderModels()->GetComponentStateForDevicePath(model,"button_b",hands[hand],&mode,&state);down=active&&(state.uProperties&vr::VRComponentProperty_IsPressed);} if(launcher_close_button.update(active,down,true,now))launcher_back(); } vr::InputAnalogActionData_t axis{};bool active=input->GetAnalogActionData(navigate_action,&axis,sizeof(axis),hands[hand])==vr::VRInputError_None&&axis.bActive;float x=axis.x,y=axis.y; if(!active&&device!=vr::k_unTrackedDeviceIndexInvalid&&vr_system->IsTrackedDeviceConnected(device)){char model[256]{};vr_system->GetStringTrackedDeviceProperty(device,vr::Prop_RenderModelName_String,model,sizeof(model));vr::RenderModel_ControllerMode_State_t mode{};vr::RenderModel_ComponentState_t rest{},full_x{},full_y{},current{};vr::VRControllerState_t neutral{},sx{},sy{};sx.rAxis[0].x=1;sy.rAxis[0].y=1;auto* models=vr::VRRenderModels();active=models->GetComponentStateForDevicePath(model,"thumbstick",hands[hand],&mode,¤t)&&models->GetComponentState(model,"thumbstick",&neutral,&mode,&rest)&&models->GetComponentState(model,"thumbstick",&sx,&mode,&full_x)&&models->GetComponentState(model,"thumbstick",&sy,&mode,&full_y)&&launcher_stick_axes(rest,full_x,full_y,current,x,y);} // Pick a single controller and a single axis; never emit two directions. if(active&&(!stick_active||std::max(std::abs(x),std::abs(y))>std::max(std::abs(stick_x),std::abs(stick_y)))){stick_active=true;stick_x=x;stick_y=y;stick_device=device;} } launcher_gaze(input,gaze_allowed,gaze_connected,gaze_buttons,now); if(launcher_open){const bool blocked=keyboard_open||launcher_dock_interaction();int direction=launcher_stick.update(blocked?0:stick_x,blocked?0:stick_y,!blocked&&stick_active);if(direction&&views.count("launcher")){auto* frame=views.at("launcher")->browser->get_main_frame(views.at("launcher")->browser);execute(frame,"window.dispatchEvent(new CustomEvent('framely.launcher.navigate',{detail:"+std::to_string(direction)+"}))");frame->base.release(&frame->base);}} else{launcher_stick={};launcher_close_button={};} page_scroll(stick_x,stick_y,stick_active,stick_device,now); if(gaze_allowed&&gaze_active)launcher_outside_at={}; if(launcher_open&&launcher_outside_at.time_since_epoch().count()&&now>=launcher_outside_at){launcher_outside_at={};if(!keyboard_open&&!launcher_dock_interaction())launcher_back();} } static void commands(){std::deque commands;{std::lock_guard lock(queue_mutex);commands.swap(queue);}for(const auto& c:commands){try{auto kind=c.value("kind","");if(kind=="feedback.settings"){ui_haptics=c.value("haptics",true);ui_sounds=c.value("sounds",false);if(!ui_sounds)ui_audio.silence();}else if(kind=="file.dialog.complete"){for(auto& [key,view]:views){auto& v=*view;if(!v.file_dialog||v.file_dialog_id!=c.value("id",std::string{}))continue;auto paths=c.value("paths",json::array());if(paths.empty()){cancel_file_dialog(v);break;}if((!v.file_multiple&&paths.size()!=1)||paths.size()>64)break;auto* callback=v.file_dialog;v.file_dialog=nullptr;v.file_dialog_id.clear();auto list=cef_string_list_alloc();for(const auto& path:paths){cef_string_t value{};str(value,path.get());cef_string_list_append(list,&value);cef_string_utf16_clear(&value);}callback->cont(callback,list);cef_string_list_free(list);callback->base.release(&callback->base);break;}}else if(kind=="gaze.correction"){gaze_correction=c.value("calibration",json(nullptr));}else if(kind=="gaze.target"){gaze_target_id=c.value("id",std::string{});gaze_target_u=c.value("x",.5f);gaze_target_v=1-c.value("y",.5f);gaze_preview=c.value("preview",json(nullptr));gaze_target_cached=false;}else if(kind=="launcher.calibrate"){gaze_target_id.clear();gaze_target_cached=false;gaze_preview=nullptr;launcher_calibrating=c.value("active",true);if(launcher_calibrating&&!launcher_open){if(!views.count("launcher"))create_view("launcher","Framely launcher",origin+"/launcher#calibrate",true,1440,800);toggle_entry(true);}if(views.count("launcher")){auto& v=*views.at("launcher");auto* frame=v.browser->get_main_frame(v.browser);execute(frame,std::string("location.hash=")+(launcher_calibrating?"'#calibrate'":"''")+";window.dispatchEvent(new Event('hashchange'))");frame->base.release(&frame->base);}}else if(kind=="launcher.close"){if(c.contains("launcherSession")&&!c["launcherSession"].is_null()&&c["launcherSession"]!=launcher_session)continue;bool handoff=c.value("handoff",false);hide_launcher(!handoff,handoff);}else if(kind=="menu.open"){hide_launcher(false);auto& menu=create_view("menu","Framely",origin+"/",false,600,840);navigate(menu,"#plugin:"+c.at("plugin").get());menu_open=true;launcher_open=false;menu_button.opened(std::chrono::steady_clock::now());}else if(kind=="haptic"){auto key=c.value("view","");if(views.count(key)&&views.at(key)->shown&&!views.at(key)->closing){auto& view=*views.at(key);bool activation=c.value("feedback","hover")=="activate";if(activation&&key=="launcher"&&gaze_active&&launcher_gaze_allowed()){auto now=std::chrono::steady_clock::now();if(now-view.activation_at>=std::chrono::milliseconds(80)){view.activation_at=now;if(ui_sounds)ui_audio.play(true);}}else hover_haptic(view.overlay,activation?view.activation_at:view.haptic_at,activation);}}else if(kind=="manager.open"){hide_launcher(false);if(views.count("framely.manager")&&views.at("framely.manager")->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}auto page=c.value("page",std::string{});auto& manager=create_view("framely.manager","Framely",origin+"/manager"+((manager_navigation_page(page)||page.rfind("plugin:",0)==0)?"#"+page:""),true,1440,810);if(page.rfind("plugin:",0)==0)navigate(manager,"#"+page);if(manager_navigation_page(page)){auto* frame=manager.browser->get_main_frame(manager.browser);execute(frame,"location.hash="+json(page).dump()+";window.dispatchEvent(new Event(\"hashchange\"))");frame->base.release(&frame->base);}menu_open=false;launcher_open=false;}else if(kind=="agreement.decline"){auto key=c.value("view","");if(key=="menu"){menu_open=false;if(views.count("menu")){show(*views.at("menu"),false);views.at("menu")->browser->reload(views.at("menu")->browser);}}else if(views.count(key))close_view(*views.at(key));}else if(kind=="manager.close"){if(views.count("framely.manager"))close_view(*views.at("framely.manager"));}else if(kind=="menu.close"){menu_open=false;}else if(kind=="window.open"){hide_launcher(false);std::string plugin=c.at("plugin"),win=c.at("window");auto key="framely.window."+plugin+"."+win;if(views.count(key)&&views.at(key)->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}auto spec=c.at("spec");auto& opened=create_view("framely.window."+plugin+"."+win,spec.at("title"),origin+"/window/"+plugin+"/"+win,spec.value("dockIcon",false),spec.value("width",1600),spec.value("height",900),plugin,spec.value("widthMeters",3.f));plugin_thumbnail(opened,spec.value("iconData",std::string{}));menu_open=false;launcher_open=false;}else if(kind=="window.close"){auto key="framely.window."+c.at("plugin").get()+"."+c.at("window").get();if(views.count(key))close_view(*views.at(key));}else if(kind=="plugin.disabled"){for(auto&[key,v]:views)if(v->plugin==c.at("plugin"))close_view(*v);}else if(kind=="notification.changed"){auto& v=create_view("notifications","Framely notifications",origin+"/notifications",false,600,360);anchor_notification(v);v.host->was_hidden(v.host,0);v.host->invalidate(v.host,PET_VIEW);}else if(kind=="keyboard"){auto key=c.value("view","menu");if(!views.count(key))continue;auto& v=*views.at(key);if(v.closing)continue;close_keyboard();v.host->set_focus(v.host,1);keyboard_frame=v.browser->get_focused_frame(v.browser);keyboard_view=key;keyboard_target=v.overlay;++keyboard_token;auto existing=c.value("existing","");if(key=="menu")overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_WantsModalBehavior,false);auto e=overlays->ShowKeyboardForOverlay(v.overlay,c.value("password",false)?vr::k_EGamepadTextInputModePassword:vr::k_EGamepadTextInputModeNormal,c.value("multiline",false)?vr::k_EGamepadTextInputLineModeMultipleLines:vr::k_EGamepadTextInputLineModeSingleLine,vr::KeyboardFlag_Modal|vr::KeyboardFlag_Minimal|vr::KeyboardFlag_ShowArrowKeys,"Framely",4096,existing.c_str(),keyboard_token);keyboard_open=e==vr::VROverlayError_None;keyboard_menu.begin(keyboard_open&&key=="menu");keyboard_state(keyboard_open);if(!keyboard_open)close_keyboard();}else if(kind=="notifications.empty"){if(views.count("notifications")){views.at("notifications")->notification_content=false;show(*views.at("notifications"),false);}}}catch(const std::exception& e){std::cerr<<"Native command error: "<IsHoverTargetOverlay(v.overlay))launcher_back();else if(v.key=="menu"&&!keyboard_open&&!keyboard_menu.holds(std::chrono::steady_clock::now())&&menu_button.can_cancel(std::chrono::steady_clock::now())){menu_open=false;std::cout<<"Menu closed: outside click"<get_main_frame(v.browser);execute(frame,"window.dispatchEvent(new Event('framely.back'))");frame->base.release(&frame->base);}break;case vr::VREvent_FocusLeave:if(v.key=="launcher"&&v.launcher_pointer.holding(std::chrono::steady_clock::now()))break;if(v.key!="notifications"||!v.notification_click.active)v.host->send_mouse_move_event(v.host,&mouse,1);break;case vr::VREvent_MouseMove:if(v.key=="launcher")v.launcher_pointer.constrain(mouse.x,mouse.y,std::chrono::steady_clock::now());if(v.key!="notifications"||!v.notification_click.hold(mouse.x,mouse.y,std::chrono::steady_clock::now()))v.host->send_mouse_move_event(v.host,&mouse,0);break;case vr::VREvent_MouseButtonDown:case vr::VREvent_MouseButtonUp:{v.host->set_focus(v.host,1);auto button=e.data.mouse.button==vr::VRMouseButton_Right?MBT_RIGHT:MBT_LEFT;if(v.key=="launcher"&&button==MBT_LEFT){auto now=std::chrono::steady_clock::now();if(e.eventType==vr::VREvent_MouseButtonDown)v.launcher_pointer.begin(mouse.x,mouse.y,now);else{v.launcher_pointer.constrain(mouse.x,mouse.y,now);v.launcher_pointer.clear();}}if(v.key=="notifications"&&button==MBT_LEFT){auto now=std::chrono::steady_clock::now();if(e.eventType==vr::VREvent_MouseButtonDown)v.notification_click.begin(mouse.x,mouse.y,now);else v.notification_click.release(mouse.x,mouse.y,now);}mouse.modifiers=e.eventType==vr::VREvent_MouseButtonDown?EVENTFLAG_LEFT_MOUSE_BUTTON:0;v.host->send_mouse_click_event(v.host,&mouse,button,e.eventType==vr::VREvent_MouseButtonUp,1);break;}case vr::VREvent_ScrollSmooth:case vr::VREvent_ScrollDiscrete:if(v.stick_scrolling||keyboard_open||!v.pointer_valid)break;mouse=v.pointer;mouse.modifiers=0;v.host->send_mouse_wheel_event(v.host,&mouse,int(e.data.scroll.xdelta*120),int(e.data.scroll.ydelta*120));break;case vr::VREvent_OverlayGamepadFocusLost:if(!keyboard_open&&!v.dashboard&&v.key!="menu"&&v.key!="notifications"&&v.key!="launcher")close_view(v);break;default:break;}} int main(int argc,char** argv){ std::cout.setf(std::ios::unitbuf);std::string initial;int port=0;for(int i=1;i+1=0)return child; if(initial.empty()||runtime.empty()||!port||token.empty())return 2;origin="http://127.0.0.1:"+std::to_string(port);std::signal(SIGINT,stop);std::signal(SIGTERM,stop);std::signal(SIGPIPE,SIG_IGN); // Check the existing runtime without bootstrapping it before opening the renderer. vr::EVRInitError vr_error;vr_system=vr::VR_Init(&vr_error,vr::VRApplication_Background);if(vr_error){std::cerr<<"SteamVR unavailable "<FindOverlay("valve.steam.gamepadui.main",&native_main)||overlays->FindOverlay("valve.steam.gamepadui.bar",&native_dock)){std::cerr<<"Waiting for native Steam UI\n";vr::VR_Shutdown();return 3;} vr::VR_Shutdown(); // Frame's compositor requires the Overlay application type for visible UI. // The session agent and service gate this renderer on all native services. vr_system=vr::VR_Init(&vr_error,vr::VRApplication_Overlay);if(vr_error){std::cerr<<"SteamVR renderer unavailable "<width:600,v?v->height:840};}; render_handler.api.get_screen_info=[](cef_render_handler_t*,cef_browser_t* b,cef_screen_info_t* info){auto* v=find_browser(b);return render_screen_info(info,v?v->width:600,v?v->height:840,v?v->render_scale:1);}; render_handler.api.on_paint=[](cef_render_handler_t*,cef_browser_t* b,cef_paint_element_type_t type,size_t dirty_count,const cef_rect_t* dirty,const void* data,int w,int h){auto* v=find_browser(b);if(!v||v->closing||type!=PET_VIEW||w!=v->width*v->render_scale||h!=v->height*v->render_scale)return;merge_paint(v->pixels,data,w,h,dirty_count,dirty,true);for(auto& damage:v->texture_damage)damage.add(w,h,dirty_count,dirty);v->dirty=true;}; life_handler.api.on_before_popup=[](cef_life_span_handler_t*,cef_browser_t*,cef_frame_t*,int,const cef_string_t*,const cef_string_t*,cef_window_open_disposition_t,int,const cef_popup_features_t*,cef_window_info_t*,cef_client_t**,cef_browser_settings_t*,cef_dictionary_value_t**,int*){return 1;}; life_handler.api.on_before_close=[](cef_life_span_handler_t*,cef_browser_t* b){auto* v=find_browser(b);if(v){cancel_file_dialog(*v);v->closed=true;v->host->base.release(&v->host->base);v->host=nullptr;v->browser->base.release(&v->browser->base);v->browser=nullptr;}}; display_handler.api.on_console_message=[](cef_display_handler_t*,cef_browser_t* b,cef_log_severity_t,const cef_string_t* message,const cef_string_t*,int){auto* v=find_browser(b);if(v&&v->key=="launcher"){const auto value=text(message);if(value=="FRAMELY_LAUNCHER_REDUCED_MOTION=1")launcher_fade.reduced=true;else if(value=="FRAMELY_LAUNCHER_REDUCED_MOTION=0")launcher_fade.reduced=false;else if(value=="FRAMELY_GAZE_FOCUS"&&gaze_active&&launcher_gaze_allowed()){ auto now=std::chrono::steady_clock::now();if(now-gaze_feedback_at>=std::chrono::milliseconds(180)){gaze_feedback_at=now;if(ui_sounds)ui_audio.play();int hand=gaze_feedback_hand;auto device=vr_system->GetTrackedDeviceIndexForControllerRole(hand?vr::TrackedControllerRole_RightHand:vr::TrackedControllerRole_LeftHand);if(device==vr::k_unTrackedDeviceIndexInvalid){hand=1-hand;device=vr_system->GetTrackedDeviceIndexForControllerRole(hand?vr::TrackedControllerRole_RightHand:vr::TrackedControllerRole_LeftHand);}if(ui_haptics&&device!=vr::k_unTrackedDeviceIndexInvalid&&vr::VRInput()->TriggerHapticVibrationAction(gaze_haptic_action,0,.012f,110.f,.16f,hands[hand])!=vr::VRInputError_None)vr_system->TriggerHapticPulse(device,0,1200);} return 1;}}std::cout<<"UI ["<<(v?v->key:"loading")<<"] "<get_url(r);auto s=text(uri);cef_string_userfree_utf16_free(uri);auto* v=find_browser(b);return int(!allow_plugin_navigation(s,origin,!f||f->is_main(f),v&&!v->plugin.empty()));}; request_handler.api.on_render_process_terminated=[](cef_request_handler_t*,cef_browser_t* b,cef_termination_status_t,int,const cef_string_t*){auto* v=find_browser(b);if(v){std::cerr<<"Renderer crashed: "<key<<"\n";close_view(*v);if(v->key=="menu")running=0;if(v->key=="launcher")launcher_open=false;}}; dialog_handler.api.on_file_dialog=[](cef_dialog_handler_t*,cef_browser_t* b,cef_file_dialog_mode_t mode,const cef_string_t*,const cef_string_t* default_path,cef_string_list_t filters,cef_string_list_t extensions,cef_string_list_t,cef_file_dialog_callback_t* callback){ auto* v=find_browser(b);if(!v||v->key!="framely.manager"||mode==FILE_DIALOG_SAVE)return 0; cancel_file_dialog(*v);callback->base.add_ref(&callback->base);v->file_dialog=callback;v->file_multiple=mode==FILE_DIALOG_OPEN_MULTIPLE;v->file_directory=mode==FILE_DIALOG_OPEN_FOLDER;v->file_dialog_id=std::to_string(++file_dialog_sequence)+"-"+std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); json accept=json::array();for(auto list:{filters,extensions}){if(!list)continue;for(size_t i=0;ibrowser->get_main_frame(v->browser);execute(frame,"window.dispatchEvent(new CustomEvent('framely.fileDialog',{detail:"+json({{"id",v->file_dialog_id},{"multiple",v->file_multiple},{"directory",v->file_directory},{"accept",accept},{"defaultPath",default_path?text(default_path):std::string{}}}).dump()+"}))");frame->base.release(&frame->base);return 1; }; client.api.get_dialog_handler=[](cef_client_t*){return dialog_handler.acquire();};client.api.get_render_handler=[](cef_client_t*){return render_handler.acquire();};client.api.get_life_span_handler=[](cef_client_t*){return life_handler.acquire();};client.api.get_display_handler=[](cef_client_t*){return display_handler.acquire();};client.api.get_request_handler=[](cef_client_t*){return request_handler.acquire();}; vr::VROverlayHandle_t button=0;auto err=overlays->CreateOverlay("framely.dock.button","Framely",&button);if(err){cef_shutdown();vr::VR_Shutdown();return 6;}auto pixels=dock_icon(false,false);DockIconTexture icon_texture;icon_texture.init();if(!icon_texture.submit(button,pixels)){icon_texture.destroy();overlays->DestroyOverlay(button);cef_shutdown();vr::VR_Shutdown();return 6;}overlays->SetOverlayInputMethod(button,vr::VROverlayInputMethod_Mouse);vr::HmdVector2_t scale{{dock_canvas_width,dock_canvas_height}};overlays->SetOverlayMouseScale(button,&scale);vr::VROverlayIntersectionMaskPrimitive_t mask{};mask.m_nPrimitiveType=vr::OverlayIntersectionPrimitiveType_Rectangle;mask.m_Primitive.m_Rectangle={dock_icon_left,0,128,128};if(overlays->SetOverlayIntersectionMask(button,&mask,1)){std::cerr<<"Dock input mask unavailable\n";overlays->DestroyOverlay(button);icon_texture.destroy();cef_shutdown();vr::VR_Shutdown();return 6;}overlays->SetOverlayFlag(button,vr::VROverlayFlags_VisibleInDashboard,true);overlays->SetOverlayFlag(button,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,true);overlays->SetOverlayFlag(button,vr::VROverlayFlags_SortWithNonSceneOverlays,false);overlays->SetOverlaySortOrder(button,0);overlays->SetOverlayFlag(button,vr::VROverlayFlags_HideLaserIntersection,false); try{create_view("menu","Framely",initial,false,600,840);}catch(const std::exception& e){std::cerr< lock(queue_mutex);auto feedback=data.value("feedback",json::object());if(!feedback.is_object())feedback=json::object();if(feedback!=last_feedback){queue.push_back({{"kind","feedback.settings"},{"haptics",feedback.value("haptics",true)},{"sounds",feedback.value("sounds",false)}});last_feedback=feedback;}auto calibration=data.value("gazeCalibration",json(nullptr));if(!gaze_loaded||calibration!=last_gaze){queue.push_back({{"kind","gaze.correction"},{"calibration",calibration}});last_gaze=calibration;gaze_loaded=true;}for(const auto& c:data.at("commands")){if(queue.size()<256)queue.push_back(c);}failures=0;}catch(const std::exception& e){if(++failures>=10){std::cerr<<"Agent connection lost: "<PollNextEvent(&global,sizeof(global))){if(global.eventType==vr::VREvent_Quit){vr_system->AcknowledgeQuit_Exiting();running=0;}observe_dashboard(global);keyboard_event(global);}launcher_priority_active.store(launcher_open&&launcher_prioritize_ui.load());vr::VROverlayHandle_t dock=0;vr::HmdVector2_t mouse_scale{};bool visible=!overlays->FindOverlay("valve.steam.gamepadui.bar",&dock)&&overlays->IsDashboardVisible()&&overlays->IsOverlayVisible(dock)&&!overlays->GetOverlayMouseScale(dock,&mouse_scale); if(visible&&(!was_visible||dock!=last_dock)){uv={};anchored_at={};sampled_at={};}last_dock=dock; // Width and aspect are only ray-solver seeds; physical size comes from intersections. float width=0,height=0;bool fresh=visible&&!overlays->GetOverlayWidthInMeters(dock,&width)&&std::isfinite(width)&&width>0&&width<10&&std::isfinite(mouse_scale.v[0])&&std::isfinite(mouse_scale.v[1])&&mouse_scale.v[0]>0&&mouse_scale.v[1]>0; if(fresh){height=width*mouse_scale.v[1]/mouse_scale.v[0];fresh=std::isfinite(height)&&height>.005f&&height<2;} vr::HmdMatrix34_t center{},anchor{};vr::HmdVector3_t head{};DockCurveEvidence evidence{};float size=0;bool anchor_ok=false; vr::HmdMatrix34_t frame{};bool frame_ok=visible&&fresh&&!overlays->GetTransformForOverlayCoordinates(dock,vr::TrackingUniverseStanding,{{mouse_scale.v[0]*.5f,mouse_scale.v[1]*.5f}},¢er)&&dock_rigid_frame(center,frame); auto sample_now=std::chrono::steady_clock::now(); if(frame_ok&&sample_now-sampled_at>=std::chrono::milliseconds(100)){sampled_at=sample_now;anchor_ok=dock_probe_origin(center,head)&&dock_uv_anchor(overlays,dock,center,head,width,height,anchor,evidence,size,uv);} auto now=std::chrono::steady_clock::now();if(anchor_ok){if(evidence.radius>0)notification_curve_radius=evidence.radius;saved_anchor=anchor;saved_frame=frame;saved_size=size;saved_evidence=evidence;anchored_at=now;}else if(frame_ok&&now-anchored_atIsOverlayVisible(button))overlays->ShowOverlay(button);if(views.count("menu")){MenuPlacement popup{};auto& v=*views.at("menu");vr::HmdMatrix34_t main_pose{};vr::VROverlayHandle_t main_overlay=0;vr::HmdVector2_t main_scale{};const vr::HmdMatrix34_t* main=nullptr; for(auto&[key,view]:views)if(view->dashboard&&view->shown&&!view->closing)main_overlay=view->overlay; if(!main_overlay)main_overlay=active_main_window(); if(main_overlay&&!overlays->GetOverlayMouseScale(main_overlay,&main_scale)&&!overlays->GetTransformForOverlayCoordinates(main_overlay,vr::TrackingUniverseStanding,{{main_scale.v[0]*.5f,main_scale.v[1]*.5f}},&main_pose))main=&main_pose; if(evidence.radius>0?dock_menu_placement(anchor,size,evidence.radius,evidence.pre_curve_pitch,popup):menu_placement(anchor,size,popup,0,main)){v.placement.apply(overlays,v.overlay,popup.transform,popup.width);v.placement.curve(overlays,v.overlay,popup.width,popup.radius);show(v,menu_open);}else show(v,false);}}else if(!keyboard_menu.holds(now)){if(overlays->IsOverlayVisible(button))overlays->ClearOverlayCursorPositionOverride(button);overlays->HideOverlay(button);if(views.count("menu"))show(*views.at("menu"),false);} if(was_visible&&!visible&&!keyboard_open&&!keyboard_menu.holds(now)){menu_button.pressed=false;menu_open=false;if(!overlays->IsDashboardVisible())hide_launcher(false,true);for(auto&[key,v]:views)if(!v->dashboard&&key!="menu"&&key!="notifications"&&key!="launcher")close_view(*v);}was_visible=visible; if(launcher_close_at.time_since_epoch().count()&&now>=launcher_close_at){launcher_close_at={};if(!launcher_open)finish_launcher_close();} // The runtime owns dashboard placement, curvature, visibility and switching. // Never move the launcher into standing space or hide another app's overlay. if(launcher_open)launcher_dock_interaction(); if(views.count("launcher")){ bool selected=overlays->IsActiveDashboardOverlay(views.at("launcher")->overlay); if(launcher_open&&selected)launcher_selected=true; if(launcher_open&&selected&&overlays->IsDashboardVisible()&&overlays->IsOverlayVisible(views.at("launcher")->overlay)&&views.at("launcher")->texture_ready[views.at("launcher")->texture_slot])launcher_fade.ready(now); if(launcher_open||launcher_close_at.time_since_epoch().count())overlays->SetOverlayAlpha(views.at("launcher")->overlay,launcher_open?std::max(.02f,launcher_fade.value(now)):launcher_fade.value(now)); if(launcher_open&&!selected&&overlays->IsDashboardVisible()&&!launcher_dock_interaction()&&(launcher_selected||now-launcher_dismiss.opened_at>std::chrono::seconds(5))){hide_launcher(false,true);} } auto activate_entry=[&](EntryHold::Action action){ if(action==EntryHold::Action::Idle)return; if(ui_sounds)ui_audio.play(true); toggle_entry(action==EntryHold::Action::Launcher); if(menu_open&&views.count("menu"))views.at("menu")->host->set_focus(views.at("menu")->host,1); }; vr::VREvent_t event{};while(overlays->PollNextOverlayEvent(button,&event,sizeof(event))){ laser_cursor(button,event); if(event.eventType==vr::VREvent_MouseMove||event.eventType==vr::VREvent_MouseButtonDown||event.eventType==vr::VREvent_MouseButtonUp)entry_pointer.move(event.trackedDeviceIndex,event.data.mouse.x,event.data.mouse.y); if(event.eventType==vr::VREvent_FocusLeave)entry_pointer.leave(); if(event.eventType==vr::VREvent_MouseButtonDown||event.eventType==vr::VREvent_MouseButtonUp){ // Cancel any existing gaze gesture before toggling views. This also // covers a complete down/up pair drained in one frame and unknown devices. if(event.eventType==vr::VREvent_MouseButtonDown&&anchor_ok&&entry_pointer.inside){ auto edge=gaze_press.cancel();if(gaze_active||edge==LauncherGazePress::Cancel)launcher_gaze_event({{"valid",false},{"phase","cancel"}});gaze_active=false; } const auto received=std::chrono::steady_clock::now();const bool was_pressed=entry_hold.pressed;const auto previous_started=entry_hold.started; 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,entry_event_time(received,event.eventAgeSeconds),received)); if(entry_hold.pressed&&(!was_pressed||entry_hold.started!=previous_started)){ entry_source_buttons=0;vr::VRControllerState_t state{}; if(entry_hold.device!=vr::k_unTrackedDeviceIndexInvalid&&vr_system->GetControllerState(entry_hold.device,&state,sizeof(state))){ const auto previous=entry_previous_buttons.find(entry_hold.device); if(previous!=entry_previous_buttons.end())entry_source_buttons=state.ulButtonPressed&~previous->second; // An unambiguous single button is safe even if its down event arrived late. if(!entry_source_buttons&&state.ulButtonPressed&&(state.ulButtonPressed&(state.ulButtonPressed-1))==0)entry_source_buttons=state.ulButtonPressed; } } } } if(!anchor_ok)entry_pointer.leave(); const bool entry_connected=entry_hold.device==vr::k_unTrackedDeviceIndexInvalid||vr_system->IsTrackedDeviceConnected(entry_hold.device); const bool entry_inside=entry_pointer.inside&&entry_pointer.device==entry_hold.device; if(entry_hold.pressed&&entry_source_buttons){vr::VRControllerState_t state{};if(vr_system->GetControllerState(entry_hold.device,&state,sizeof(state))&&!(state.ulButtonPressed&entry_source_buttons))activate_entry(entry_hold.released(anchor_ok&&entry_connected,entry_inside));} activate_entry(entry_hold.tick(std::chrono::steady_clock::now(),anchor_ok&&entry_connected,entry_inside)); 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);} for(auto&[key,v]:views){if(v->closing)continue;if(v->dashboard){bool active=overlays->IsDashboardVisible()&&overlays->IsActiveDashboardOverlay(v->overlay)&&overlays->IsOverlayVisible(v->overlay);if(active!=v->shown){if(!active&&key=="launcher"){if(v->launcher_pointer.active){cef_mouse_event_t outside{};outside.modifiers=EVENTFLAG_LEFT_MOUSE_BUTTON;v->host->send_mouse_move_event(v->host,&outside,0);outside.modifiers=0;v->host->send_mouse_click_event(v->host,&outside,MBT_LEFT,1,1);}v->launcher_pointer.clear();auto* frame=v->browser->get_main_frame(v->browser);execute(frame,"window.dispatchEvent(new Event('framely.launcher.cancelInput'))");frame->base.release(&frame->base);}v->host->was_hidden(v->host,!active);v->shown=active;}if(!active)v->active_seen=false;if(active&&!v->active_seen){v->active_seen=true;if(key!="launcher")for(auto&[other,t]:views)if(!t->dashboard&&other!="menu"&&other!="notifications"&&other!="launcher"&&!keyboard_open)close_view(*t);}} while(overlays->PollNextOverlayEvent(v->overlay,&event,sizeof(event)))overlay_input(*v,event); if(v->dirty&&!v->closing){ glXMakeContextCurrent(xdisplay,glsurface,glsurface,glcontext); // Upload to the other texture; never redefine storage submitted this frame. const int slot=1-v->texture_slot;const GLuint id=v->textures[slot]; auto& damage=v->texture_damage[slot];const int pixel_width=v->width*v->render_scale,pixel_height=v->height*v->render_scale; static auto generate_mipmap=reinterpret_cast(glXGetProcAddressARB(reinterpret_cast("glGenerateMipmap"))); bool mipmapped=v->key=="launcher"&&generate_mipmap; glBindTexture(GL_TEXTURE_2D,id); glPixelStorei(GL_UNPACK_ALIGNMENT,4);glPixelStorei(GL_UNPACK_ROW_LENGTH,0); glPixelStorei(GL_UNPACK_SKIP_PIXELS,0);glPixelStorei(GL_UNPACK_SKIP_ROWS,0); if(!v->texture_ready[slot]){ glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,mipmapped?GL_LINEAR_MIPMAP_LINEAR:GL_LINEAR);glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,v->width*v->render_scale,v->height*v->render_scale,0,GL_BGRA,GL_UNSIGNED_BYTE,nullptr); v->texture_ready[slot]=true; damage.include(0,0,pixel_width,pixel_height); } if(!damage.empty()){ glPixelStorei(GL_UNPACK_ROW_LENGTH,pixel_width); glTexSubImage2D(GL_TEXTURE_2D,0,damage.x,damage.y,damage.width,damage.height,GL_BGRA,GL_UNSIGNED_BYTE,v->pixels.data()+(size_t(damage.y)*pixel_width+damage.x)*4); glPixelStorei(GL_UNPACK_ROW_LENGTH,0);damage.clear(); if(mipmapped)generate_mipmap(GL_TEXTURE_2D); } // glFlush queues the upload; glFinish makes the full image ready for sharing. glFinish();vr::Texture_t texture{reinterpret_cast(uintptr_t(id)),vr::TextureType_OpenGL,vr::ColorSpace_Auto}; auto e=overlays->SetOverlayTexture(v->overlay,&texture);glFlush(); if(e)std::cerr<<"Texture submission failed "<texture_slot=slot;v->dirty=false; if(key=="notifications"){ // Visibility follows the submitted frame, never a queue-change event. // Keep CEF painting while the overlay is hidden so a later toast can appear. // A transparent frame disables input even if the page stays mounted. auto bounds=notification_bounds(v->pixels,v->width*v->render_scale,v->height*v->render_scale,v->render_scale); bool visible=bounds.visible(); if(visible){ vr::VROverlayIntersectionMaskPrimitive_t mask{};mask.m_nPrimitiveType=vr::OverlayIntersectionPrimitiveType_Rectangle; mask.m_Primitive.m_Rectangle={bounds.x,bounds.y,bounds.width,bounds.height}; auto error=overlays->SetOverlayIntersectionMask(v->overlay,&mask,1); if(error){std::cerr<<"Notification input mask failed "<notification_content=visible; } } } if(key=="notifications"){ if(v->notification_click.active&&std::chrono::steady_clock::now()-v->notification_click.pressed>std::chrono::milliseconds(800)){ // A release outside the overlay may not arrive. Never leave CEF pressed. cef_mouse_event_t outside{};outside.x=outside.y=-1; v->host->send_mouse_click_event(v->host,&outside,MBT_LEFT,1,1);v->notification_click.clear(); } if(v->notification_content&&!v->placement.valid)anchor_notification(*v); if(v->placement.valid)v->placement.curve(overlays,v->overlay,v->placement.width,notification_curve_radius); show(*v,v->notification_content&&v->placement.valid); } } {json snapshot=json::object();for(auto&[key,v]:views){if(key=="menu"||key=="launcher"||key=="framely.manager"||key.rfind("framely.window.",0)==0)snapshot[key]=capture_view_visible(v->closing,overlays->IsOverlayVisible(v->overlay),v->dashboard,overlays->IsDashboardVisible(),overlays->IsActiveDashboardOverlay(v->overlay));}std::lock_guard lock(visibility_mutex);visible_views=std::move(snapshot);} if(keyboard_close_pending){ // A non-minimal runtime keyboard may buffer text until Done. Drain overlay // character events first so the global Done event cannot duplicate input. if(keyboard_toolbar&&keyboard_done&&!keyboard_chars){char text[16385]{};overlays->GetKeyboardText(text,sizeof(text));keyboard_edit(keyboard_frame,std::string(text,strnlen(text,sizeof(text))));} 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 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; // Remember source state only after draining events, to identify the actual // newly pressed button rather than unrelated buttons held on the controller. for(auto device:{entry_pointer.device,entry_hold.device})if(device!=vr::k_unTrackedDeviceIndexInvalid){vr::VRControllerState_t state{};if(vr_system->GetControllerState(device,&state,sizeof(state)))entry_previous_buttons[device]=state.ulButtonPressed;else entry_previous_buttons.erase(device);} 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. if(sync!=vr::VROverlayError_None&&sync!=vr::VROverlayError_TimedOut)std::this_thread::sleep_until(frame_start+std::chrono::milliseconds(8)); } close_keyboard();for(auto&[key,v]:views)close_view(*v);auto deadline=std::chrono::steady_clock::now()+std::chrono::seconds(5);while(std::chrono::steady_clock::now()closed;if(all)break;std::this_thread::sleep_for(std::chrono::milliseconds(5));}poller.join();priority_worker.join();for(auto&[key,v]:views)if(!v->closed){std::cerr<<"Browser shutdown timed out\n";_Exit(7);}for(auto&[key,v]:views){overlays->DestroyOverlay(v->overlay);if(v->thumbnail)overlays->DestroyOverlay(v->thumbnail);glDeleteTextures(2,v->textures);}views.clear();overlays->DestroyOverlay(button);icon_texture.destroy();cef_shutdown();vr::VR_Shutdown();glXMakeContextCurrent(xdisplay,None,None,nullptr);glXDestroyContext(xdisplay,glcontext);glXDestroyPbuffer(xdisplay,glsurface);XCloseDisplay(xdisplay);return 0; }