mirror of
https://github.com/SteamFramelyHomebrew/framely.git
synced 2026-10-23 08:02:39 +02:00
feat(headset): connect screenshot, radial, and keyboard runtime flows
Wire native overlays, screenshot sessions, radial actions, and keyboard interception through the host and session APIs. Keep the progress ring in a separate CEF view so repeat long presses cannot flash the previous menu frame.
This commit is contained in:
1 parent
4746357769
commit
f9f14947ee
3 files changed
+824
-29
No files matched your search
+417
-20
@@ -34,12 +34,15 @@
|
||||
#include "recording_indicator.h"
|
||||
#include "recording_region.h"
|
||||
#include "recording_region_depth.h"
|
||||
#include "system_radial.h"
|
||||
#include "framing_lifecycle.h"
|
||||
#include "plugin_navigation.h"
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glx.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <chrono>
|
||||
@@ -118,6 +121,15 @@ 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;
|
||||
constexpr const char* frame_keyboard_window_key="framely.window.tooru.frame-keyboard.keyboard";
|
||||
struct ExternalKeyboard {
|
||||
bool active=false,hide_pending=false;
|
||||
vr::VROverlayHandle_t target=0;
|
||||
uint64_t token=0;
|
||||
std::string overlay_key,view_key;
|
||||
std::chrono::steady_clock::time_point hide_pending_until{};
|
||||
};
|
||||
static ExternalKeyboard external_keyboard;
|
||||
static std::mutex queue_mutex;
|
||||
static std::mutex visibility_mutex;
|
||||
static json visible_views=nullptr;
|
||||
@@ -140,6 +152,7 @@ struct View {
|
||||
vr::VROverlayHandle_t overlay=0,thumbnail=0;GLuint textures[2]{};int texture_slot=0;bool texture_ready[2]{};PaintDamage texture_damage[2];std::vector<uint8_t> pixels;
|
||||
};
|
||||
static std::map<std::string,std::unique_ptr<View>> views;
|
||||
static void close_view(View& v);
|
||||
static bool permission_trigger_held[2]{};
|
||||
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<cef_client_t> client;
|
||||
@@ -178,10 +191,87 @@ static std::string pending_recording_stop;
|
||||
static json recording_state=json::object();
|
||||
static json recording_region_state=json::object();
|
||||
static json framing_editor_state=nullptr;
|
||||
static json screenshot_editor_state=nullptr;
|
||||
static json screenshot_shortcut_preview=nullptr;
|
||||
static bool screenshot_radial_held=false;
|
||||
static bool system_radial_available=false;
|
||||
static std::atomic<bool> system_radial_enabled{true};
|
||||
static SystemRadial system_radial;
|
||||
static bool system_radial_preview_prepared=false;
|
||||
static bool system_radial_progress_visible=false;
|
||||
static vr::HmdVector3_t system_radial_origin{};
|
||||
static vr::HmdVector3_t system_radial_right{};
|
||||
static vr::HmdVector3_t system_radial_up{};
|
||||
static bool system_radial_origin_valid=false;
|
||||
static vr::HmdMatrix34_t system_radial_pose[2]{};
|
||||
static SystemRadial::Clock::time_point system_radial_pose_at[2]{};
|
||||
static vr::TrackedDeviceIndex_t system_radial_pose_device[2]{vr::k_unTrackedDeviceIndexInvalid,vr::k_unTrackedDeviceIndexInvalid};
|
||||
static std::chrono::steady_clock::time_point system_radial_sent{};
|
||||
static std::string pending_screenshot_hotkey,pending_screenshot_close,pending_system_action;
|
||||
static uint64_t system_action_sequence=0;
|
||||
static RecordingRegionOverlay screenshot_region_overlay{"framely.screenshot.region"};
|
||||
static std::string pending_region_close;
|
||||
static RecordingRegionOverlay recording_region_overlay;
|
||||
static RecordingIndicator recording_indicator;
|
||||
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<sockaddr*>(&addr),sizeof(addr))){close(fd);throw std::runtime_error("UI agent unavailable");}std::string payload;size_t closed_count=0;std::string stop_id,region_closed;{std::lock_guard<std::mutex> lock(visibility_mutex);closed_count=closed_casts.size();stop_id=pending_recording_stop;region_closed=pending_region_close;payload=json{{"views",visible_views},{"closedCasts",closed_casts},{"stopRecording",stop_id},{"framingClosed",region_closed}}.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(sent<req.size()){auto n=send(fd,req.data()+sent,req.size()-sent,MSG_NOSIGNAL);if(n<=0){close(fd);throw std::runtime_error("HTTP send failed");}sent+=n;}std::string response;char buffer[4096];ssize_t n;while((n=recv(fd,buffer,sizeof(buffer),0))>0){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");auto parsed=json::parse(response.substr(body+4));{std::lock_guard<std::mutex> lock(visibility_mutex);if(pending_region_close==region_closed)pending_region_close.clear();if(pending_recording_stop==stop_id)pending_recording_stop.clear();closed_casts.erase(closed_casts.begin(),closed_casts.begin()+std::min(closed_count,closed_casts.size()));}return parsed;}
|
||||
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<sockaddr*>(&addr),sizeof(addr))){close(fd);throw std::runtime_error("UI agent unavailable");}std::string payload;size_t closed_count=0;std::string stop_id,region_closed,screenshot_closed,hotkey,system_action;{std::lock_guard<std::mutex> lock(visibility_mutex);closed_count=closed_casts.size();stop_id=pending_recording_stop;region_closed=pending_region_close;screenshot_closed=pending_screenshot_close;hotkey=pending_screenshot_hotkey;system_action=pending_system_action;payload=json{{"views",visible_views},{"closedCasts",closed_casts},{"stopRecording",stop_id},{"framingClosed",region_closed},{"screenshotClosed",screenshot_closed},{"screenshotHotkey",hotkey},{"radialAction",system_action}}.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(sent<req.size()){auto n=send(fd,req.data()+sent,req.size()-sent,MSG_NOSIGNAL);if(n<=0){close(fd);throw std::runtime_error("HTTP send failed");}sent+=n;}std::string response;char buffer[4096];ssize_t n;while((n=recv(fd,buffer,sizeof(buffer),0))>0){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");auto parsed=json::parse(response.substr(body+4));{std::lock_guard<std::mutex> lock(visibility_mutex);if(pending_region_close==region_closed)pending_region_close.clear();if(pending_screenshot_close==screenshot_closed)pending_screenshot_close.clear();if(pending_screenshot_hotkey==hotkey)pending_screenshot_hotkey.clear();if(pending_system_action==system_action)pending_system_action.clear();if(pending_recording_stop==stop_id)pending_recording_stop.clear();closed_casts.erase(closed_casts.begin(),closed_casts.begin()+std::min(closed_count,closed_casts.size()));}return parsed;}
|
||||
static json service_call(const std::string& method,const json& params){
|
||||
const char* configured=std::getenv("FRAMELY_CONTROL_SOCKET");
|
||||
const std::string path=configured&&*configured?configured:"/run/framely/control.sock";
|
||||
sockaddr_un address{};address.sun_family=AF_UNIX;
|
||||
if(path.size()>=sizeof(address.sun_path))throw std::runtime_error("Control socket path is too long");
|
||||
std::strncpy(address.sun_path,path.c_str(),sizeof(address.sun_path)-1);
|
||||
const int fd=socket(AF_UNIX,SOCK_STREAM|SOCK_CLOEXEC,0);
|
||||
if(fd<0)throw std::runtime_error("Control socket failed");
|
||||
timeval timeout{2,0};setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));
|
||||
if(connect(fd,reinterpret_cast<sockaddr*>(&address),sizeof(address))<0){close(fd);throw std::runtime_error("Control service unavailable");}
|
||||
const std::string request=json{{"method",method},{"params",params}}.dump()+"\n";
|
||||
size_t sent=0;
|
||||
while(sent<request.size()){
|
||||
const auto count=send(fd,request.data()+sent,request.size()-sent,MSG_NOSIGNAL);
|
||||
if(count<=0){close(fd);throw std::runtime_error("Control request failed");}
|
||||
sent+=static_cast<size_t>(count);
|
||||
}
|
||||
std::string response;char buffer[4096];
|
||||
while(response.find('\n')==std::string::npos){
|
||||
const auto count=recv(fd,buffer,sizeof(buffer),0);
|
||||
if(count<=0){close(fd);throw std::runtime_error("Control response failed");}
|
||||
response.append(buffer,static_cast<size_t>(count));
|
||||
if(response.size()>1024*1024){close(fd);throw std::runtime_error("Control response is too large");}
|
||||
}
|
||||
close(fd);
|
||||
auto reply=json::parse(response.substr(0,response.find('\n')));
|
||||
if(reply.contains("error"))throw std::runtime_error(reply["error"].get<std::string>());
|
||||
return reply.value("result",json(nullptr));
|
||||
}
|
||||
static bool frame_keyboard_command(const std::string& method,const json& params){
|
||||
try{
|
||||
auto result=service_call("plugin.call",json{{"plugin","tooru.frame-keyboard"},{"method",method},{"params",params}});
|
||||
if(result.is_null())return false;
|
||||
if(result.is_object()&&result.contains("ok")&&result["ok"].is_boolean()&&!result["ok"].get<bool>()){
|
||||
std::cerr<<"Frame keyboard "<<method<<" returned ok=false";
|
||||
if(result.contains("error")&&result["error"].is_string())std::cerr<<": "<<result["error"].get<std::string>();
|
||||
std::cerr<<"\n";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}catch(const std::exception& error){
|
||||
std::cerr<<"Frame keyboard command failed: "<<error.what()<<"\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static bool plugin_handles_external_keyboard(){
|
||||
// Version 0.3 of the plugin listens to OpenVR global keyboard events itself.
|
||||
// Cache the capability briefly so a missing plugin still falls back quickly.
|
||||
static std::atomic<int> cached{-1};static std::atomic<long long> checked_at{0};
|
||||
const auto now=std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
if(cached.load()>=0&&now-checked_at.load()<5000)return cached.load()==1;
|
||||
bool enabled=false;
|
||||
try{
|
||||
auto result=service_call("plugin.call",json{{"plugin","tooru.frame-keyboard"},{"method","keyboardInterception"},{"params",json::object()}});
|
||||
enabled=result.is_object()&&result.value("enabled",false);
|
||||
}catch(...){}
|
||||
cached.store(enabled?1:0);checked_at.store(now);return enabled;
|
||||
}
|
||||
// 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
|
||||
@@ -320,26 +410,92 @@ static void show(View& v,bool shown){
|
||||
if(shown!=actual){auto e=shown?overlays->ShowOverlay(v.overlay):overlays->HideOverlay(v.overlay);if(e){std::cerr<<"Overlay visibility failed "<<v.key<<" "<<e<<"\n";return;}}
|
||||
if(shown!=v.shown){
|
||||
if(!shown){overlays->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);
|
||||
if(v.key!="notifications"&&v.key!="framely.system.radial"&&v.key!="framely.system.radial.progress")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 void external_keyboard_clear(bool notify_backend){
|
||||
const bool notify=notify_backend&&external_keyboard.active&&running;
|
||||
external_keyboard={};
|
||||
if(notify)std::thread([]{frame_keyboard_command("dashboardClose",json::object());}).detach();
|
||||
}
|
||||
static void close_external_keyboard(bool close_frame);
|
||||
static void external_keyboard_tick(std::chrono::steady_clock::time_point now){
|
||||
// Hiding SteamVR's keyboard emits a close event which must not tear down the
|
||||
// replacement. Bound that guard so a later real close is still observed.
|
||||
if(external_keyboard.active&&external_keyboard.hide_pending&&now>=external_keyboard.hide_pending_until)external_keyboard.hide_pending=false;
|
||||
// Closing the SteamVR dashboard also invalidates the X11 text target.
|
||||
if(external_keyboard.active&&!external_keyboard.hide_pending&&!overlays->IsDashboardVisible())close_external_keyboard(true);
|
||||
}
|
||||
static void close_external_keyboard(bool close_frame){
|
||||
const std::string view_key=external_keyboard.view_key;
|
||||
external_keyboard_clear(true);
|
||||
if(close_frame&&!view_key.empty()){
|
||||
auto found=views.find(view_key);
|
||||
if(found!=views.end()&&!found->second->closing)close_view(*found->second);
|
||||
}
|
||||
}
|
||||
static bool open_external_keyboard(const std::string& overlay_key,vr::VROverlayHandle_t target,uint64_t token){
|
||||
const json params={{"overlayKey",overlay_key},{"overlayHandle",target},{"token",token}};
|
||||
if(!frame_keyboard_command("dashboardOpen",params))return false;
|
||||
external_keyboard.active=true;
|
||||
external_keyboard.hide_pending=true;
|
||||
external_keyboard.target=target;
|
||||
external_keyboard.token=token;
|
||||
external_keyboard.overlay_key=overlay_key;
|
||||
external_keyboard.view_key=frame_keyboard_window_key;
|
||||
external_keyboard.hide_pending_until=std::chrono::steady_clock::now()+std::chrono::milliseconds(1500);
|
||||
overlays->HideKeyboard();
|
||||
try{
|
||||
service_call("window.open",json{{"plugin","tooru.frame-keyboard"},{"window","keyboard"}});
|
||||
}catch(const std::exception& error){
|
||||
std::cerr<<"Frame keyboard window failed: "<<error.what()<<"\n";
|
||||
external_keyboard_clear(true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
if(!owner){
|
||||
char key[1024]{};vr::EVROverlayError error{};overlays->GetOverlayKey(target,key,sizeof(key),&error);
|
||||
const std::string overlay_key=error?"":key;
|
||||
if(overlay_key.empty()||overlay_key.rfind("framely.",0)==0)return false;
|
||||
if(plugin_handles_external_keyboard())return true;
|
||||
if(external_keyboard.active&&external_keyboard.target==target){
|
||||
// A repeated Opened event during the native hide is part of the same
|
||||
// transition. After that grace period treat it as a second keyboard click.
|
||||
if(!external_keyboard.hide_pending)close_external_keyboard(true);
|
||||
overlays->HideKeyboard();
|
||||
return true;
|
||||
}
|
||||
if(!open_external_keyboard(overlay_key,target,e.data.keyboard.uUserValue))return false;
|
||||
return true;
|
||||
}
|
||||
if(owner->key==frame_keyboard_window_key){return true;}
|
||||
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;
|
||||
const bool external_target=external_keyboard.active&&e.data.keyboard.overlayHandle==external_keyboard.target;
|
||||
const bool external_close=e.eventType==vr::VREvent_KeyboardClosed||e.eventType==vr::VREvent_KeyboardClosed_Global;
|
||||
const bool external_input=!e.data.keyboard.uUserValue||!external_keyboard.token||e.data.keyboard.uUserValue==external_keyboard.token;
|
||||
if(external_target&&(external_close||external_input)){
|
||||
if(external_close){
|
||||
if(external_keyboard.hide_pending){external_keyboard.hide_pending=false;return true;}
|
||||
close_external_keyboard(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
@@ -349,8 +505,26 @@ static bool keyboard_event(const vr::VREvent_t& e,vr::VROverlayHandle_t source_o
|
||||
}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;if(v.key.rfind("framely.cast.",0)==0){std::lock_guard<std::mutex> lock(visibility_mutex);closed_casts.push_back(v.key.substr(13));}if(v.key=="framely.framing"){std::lock_guard<std::mutex> lock(visibility_mutex);pending_region_close=v.region_editor_id;}cancel_file_dialog(v);if(keyboard_view==v.key)close_keyboard();v.closing=true;if(v.key=="menu"||v.key=="framely.apk.permission")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 close_view(View& v){if(v.closing)return;if(external_keyboard.active&&v.key==external_keyboard.view_key)external_keyboard_clear(true);if(v.key==frame_keyboard_window_key){std::lock_guard<std::mutex> lock(queue_mutex);queue.erase(std::remove_if(queue.begin(),queue.end(),[](const json& command){return command.value("kind",std::string{})=="window.open"&&command.value("plugin",std::string{})=="tooru.frame-keyboard";}),queue.end());if(running)std::thread([]{frame_keyboard_command("dashboardClose",json::object());}).detach();}if(v.key.rfind("framely.cast.",0)==0){std::lock_guard<std::mutex> lock(visibility_mutex);closed_casts.push_back(v.key.substr(13));}if(v.key=="framely.framing"){std::lock_guard<std::mutex> lock(visibility_mutex);pending_region_close=v.region_editor_id;}if(v.key=="framely.screenshot"){std::lock_guard<std::mutex> lock(visibility_mutex);pending_screenshot_close=v.region_editor_id;}cancel_file_dialog(v);if(keyboard_view==v.key)close_keyboard();v.closing=true;if(v.key=="menu"||v.key=="framely.apk.permission")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_frame_keyboard(View& v){
|
||||
// A keyboard button must not move keyboard focus away from the edited app.
|
||||
// SteamVR owns the keyboard's spatial container and its native grab handle.
|
||||
vr::VROverlayHandle_t native=0;vr::HmdVector2_t scale{};float width=0;
|
||||
vr::HmdMatrix34_t pose{};
|
||||
if(!overlays->FindOverlay("valve.steam.gamepadui.keyboard",&native)&&overlays->IsOverlayVisible(native)&&
|
||||
!overlays->GetOverlayMouseScale(native,&scale)&&scale.v[0]>0&&
|
||||
!overlays->GetOverlayWidthInMeters(native,&width)&&width>0&&
|
||||
!overlays->GetTransformForOverlayCoordinates(native,vr::TrackingUniverseStanding,{{scale.v[0]*.5f,scale.v[1]*.5f}},&pose)){
|
||||
const float dy=(width*float(v.height)/v.width-width*scale.v[1]/scale.v[0])*.5f;
|
||||
for(int r=0;r<3;++r)pose.m[r][3]+=pose.m[r][1]*dy+pose.m[r][2]*.003f;
|
||||
overlays->SetOverlayWidthInMeters(v.overlay,width);
|
||||
overlays->SetOverlayTransformAbsolute(v.overlay,vr::TrackingUniverseStanding,&pose);
|
||||
float curvature=0,pitch=0;
|
||||
overlays->GetOverlayCurvature(native,&curvature);overlays->GetOverlayPreCurvePitch(native,&pitch);
|
||||
overlays->SetOverlayCurvature(v.overlay,curvature);overlays->SetOverlayPreCurvePitch(v.overlay,pitch);
|
||||
}else anchor_to_head(v,1.35f,0,-.38f,-1.2f);
|
||||
}
|
||||
static void anchor_permission(View& v){
|
||||
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]{};
|
||||
vr_system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding,0,poses,vr::k_unMaxTrackedDeviceCount);
|
||||
@@ -380,15 +554,23 @@ static void anchor_notification(View& v){
|
||||
v.placement.transform=toast.transform;v.placement.width=toast.width;v.placement.valid=true;
|
||||
v.placement.curve(overlays,v.overlay,toast.width,toast.radius);
|
||||
}
|
||||
static View& create_view(const std::string& key,const std::string& title,const std::string& url,bool dock,int width,int height,const std::string& plugin="",float physical_width=0){
|
||||
static View& create_view(const std::string& key,const std::string& title,const std::string& url,bool dock,int width,int height,const std::string& plugin="",float physical_width=0,bool keyboard_surface=false){
|
||||
// Plugin windows always use Steam window controls; dock only controls the tab.
|
||||
const bool dashboard=dock||!plugin.empty()||key.rfind("framely.cast.",0)==0;
|
||||
keyboard_surface=keyboard_surface||key==frame_keyboard_window_key;
|
||||
const bool dashboard=!keyboard_surface&&(dock||!plugin.empty()||key.rfind("framely.cast.",0)==0);
|
||||
if(views.count(key)){auto& v=*views.at(key);if(v.dashboard&&key!="launcher")overlays->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<View>();v->key=key;v->plugin=plugin;v->title=title;v->dashboard=dashboard;v->width=width;v->height=height;v->render_scale=key=="launcher"?1.5f:(key=="framely.manager"||key=="menu"||key=="notifications"||key=="framely.framing")?2:1;
|
||||
if(views.size()>=34)throw std::runtime_error("Window limit exceeded");auto v=std::make_unique<View>();v->key=key;v->plugin=plugin;v->title=title;v->dashboard=dashboard;v->width=width;v->height=height;v->render_scale=key=="launcher"?1.5f:(key=="framely.manager"||key=="menu"||key=="notifications"||key=="framely.framing"||key=="framely.screenshot"||key=="framely.system.radial"||key=="framely.system.radial.progress")?2:1;
|
||||
auto error=dashboard?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,key=="framely.apk.permission");overlays->SetOverlaySortOrder(v->overlay,key=="framely.apk.permission"?100:0);if(key=="framely.framing")recording_region_foreground(overlays,v->overlay);if(key=="framely.apk.permission")overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_WantsModalBehavior,true);
|
||||
const bool radial=key=="framely.system.radial"||key=="framely.system.radial.progress";
|
||||
overlays->SetOverlayInputMethod(v->overlay,key=="notifications"||radial?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,radial);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_SendVRSmoothScrollEvents,!radial);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_VisibleInDashboard,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,!radial);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_SortWithNonSceneOverlays,key==frame_keyboard_window_key||key=="framely.apk.permission"||radial);overlays->SetOverlaySortOrder(v->overlay,key==frame_keyboard_window_key?20:key=="framely.apk.permission"?100:radial?200:0);if(key=="framely.framing"||key=="framely.screenshot")recording_region_foreground(overlays,v->overlay);if(key=="framely.apk.permission")overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_WantsModalBehavior,true);
|
||||
if(key=="framely.system.radial"){
|
||||
vr::VROverlayIntersectionMaskPrimitive_t mask{};
|
||||
mask.m_nPrimitiveType=vr::OverlayIntersectionPrimitiveType_Circle;
|
||||
mask.m_Primitive.m_Circle={width*.5f,height*.5f,260.f};
|
||||
if(auto e=overlays->SetOverlayIntersectionMask(v->overlay,&mask,1))std::cerr<<"Radial input mask failed "<<e<<"\n";
|
||||
}
|
||||
if(key=="framely.apk.permission"){
|
||||
vr::VROverlayIntersectionMaskPrimitive_t mask{};
|
||||
mask.m_nPrimitiveType=vr::OverlayIntersectionPrimitiveType_Rectangle;
|
||||
@@ -398,17 +580,18 @@ static View& create_view(const std::string& key,const std::string& title,const s
|
||||
|
||||
// Keep the runtime's full dashboard controls, including its keyboard and close buttons.
|
||||
if(dashboard){
|
||||
overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_NoDashboardTab,!dock||key=="launcher");
|
||||
overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_NoDashboardTab,keyboard_surface||!dock||key=="launcher");
|
||||
auto result=overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_MinimalControlBar,key=="launcher");
|
||||
if(result)std::cerr<<"Dashboard controls unavailable "<<key<<" "<<result<<"\n";
|
||||
for(auto flag:{vr::VROverlayFlags_EnableControlBarKeyboard,vr::VROverlayFlags_EnableControlBarClose}){
|
||||
result=overlays->SetOverlayFlag(v->overlay,flag,true);
|
||||
result=overlays->SetOverlayFlag(v->overlay,flag,!keyboard_surface||flag==vr::VROverlayFlags_EnableControlBarClose);
|
||||
if(result)std::cerr<<"Dashboard control unavailable "<<key<<" "<<result<<"\n";
|
||||
}
|
||||
}
|
||||
if(dashboard){auto pixels=icon(false,true);overlays->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);}
|
||||
if(keyboard_surface)anchor_frame_keyboard(*v);
|
||||
else if(dashboard){auto pixels=icon(false,true);overlays->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"&&!radial){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;info.runtime_style=CEF_RUNTIME_STYLE_ALLOY;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 "<<key<<" "<<bs.windowless_frame_rate<<"\n";cef_string_t uri{};str(uri,url);auto* browser=cef_browser_host_create_browser_sync(&info,&client.api,&uri,&bs,nullptr,nullptr);cef_string_utf16_clear(&uri);
|
||||
if(!browser){overlays->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(ptr->dashboard&&key!="launcher")overlays->ShowDashboard(key.c_str());else if(key!="menu"&&key!="notifications"&&key!="launcher")show(*ptr,true);return *ptr;
|
||||
if(!browser){overlays->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"&&!radial);if(ptr->dashboard&&key!="launcher")overlays->ShowDashboard(key.c_str());else if(key!="menu"&&key!="notifications"&&key!="launcher"&&!radial)show(*ptr,true);return *ptr;
|
||||
}
|
||||
static void plugin_thumbnail(View& view,const std::string& encoded){
|
||||
if(!view.thumbnail||encoded.empty()||encoded.size()>1400000)return;
|
||||
@@ -579,6 +762,203 @@ static void launcher_gaze(vr::IVRInput* input,bool allowed,const bool (&connecte
|
||||
}
|
||||
gaze_active=true;
|
||||
}
|
||||
static const char* radial_direction(SystemRadialDirection direction){
|
||||
switch(direction){
|
||||
case SystemRadialDirection::Up:return "up";
|
||||
case SystemRadialDirection::UpRight:return "upRight";
|
||||
case SystemRadialDirection::Right:return "right";
|
||||
case SystemRadialDirection::DownRight:return "downRight";
|
||||
case SystemRadialDirection::Down:return "down";
|
||||
case SystemRadialDirection::DownLeft:return "downLeft";
|
||||
case SystemRadialDirection::Left:return "left";
|
||||
case SystemRadialDirection::UpLeft:return "upLeft";
|
||||
default:return "center";
|
||||
}
|
||||
}
|
||||
static void radial_screenshot(bool active,bool capture){
|
||||
if(active==screenshot_radial_held)return;
|
||||
screenshot_radial_held=active;
|
||||
if(!active){
|
||||
screenshot_region_overlay.destroy();
|
||||
if(auto preview=views.find("framely.screenshot.preview");preview!=views.end())close_view(*preview->second);
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(visibility_mutex);
|
||||
if(active)pending_screenshot_hotkey="radialPress";
|
||||
else if(pending_screenshot_hotkey=="radialPress")pending_screenshot_hotkey=capture?"radialCapture":"";
|
||||
else pending_screenshot_hotkey=capture?"release":"cancel";
|
||||
}
|
||||
static void system_radial_laser(vr::VROverlayHandle_t overlay,bool enabled){
|
||||
auto method=enabled?vr::VROverlayInputMethod_Mouse:vr::VROverlayInputMethod_None;
|
||||
if(auto e=overlays->SetOverlayInputMethod(overlay,method))std::cerr<<"Radial laser input failed "<<e<<"\n";
|
||||
overlays->SetOverlayFlag(overlay,vr::VROverlayFlags_HideLaserIntersection,!enabled);
|
||||
overlays->SetOverlayFlag(overlay,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,enabled);
|
||||
}
|
||||
static std::optional<SystemRadialDirection> system_radial_stick(const char* model,vr::VRInputValueHandle_t hand){
|
||||
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();float x=0,y=0;
|
||||
if(!models->GetComponentStateForDevicePath(model,"thumbstick",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)||x*x+y*y<.35f*.35f)return std::nullopt;
|
||||
return SystemRadial::select(x,y);
|
||||
}
|
||||
static void system_radial_tick(){
|
||||
if(!system_radial_available||!views.count("framely.system.radial")||!views.count("framely.system.radial.progress"))return;
|
||||
auto& progress_view=*views.at("framely.system.radial.progress");
|
||||
if(!system_radial_enabled.load()){
|
||||
auto& view=*views.at("framely.system.radial");
|
||||
if(system_radial.active||system_radial_progress_visible||system_radial.open){
|
||||
show(view,false);show(progress_view,false);system_radial_laser(view.overlay,false);overlays->ClearOverlayTexture(view.overlay);
|
||||
system_radial={};system_radial_progress_visible=false;
|
||||
if(screenshot_radial_held)radial_screenshot(false,false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto now=SystemRadial::Clock::now();
|
||||
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]{};
|
||||
vr_system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding,0,poses,vr::k_unMaxTrackedDeviceCount);
|
||||
int first=system_radial.active?system_radial.hand:0,last=system_radial.active?first:1;
|
||||
for(int hand=first;hand<=last;hand++){
|
||||
auto device=vr_system->GetTrackedDeviceIndexForControllerRole(hand?vr::TrackedControllerRole_RightHand:vr::TrackedControllerRole_LeftHand);
|
||||
if(device!=system_radial_pose_device[hand]){system_radial_pose_device[hand]=device;system_radial_pose_at[hand]={};}
|
||||
if(device!=vr::k_unTrackedDeviceIndexInvalid&&poses[device].bPoseIsValid){system_radial_pose[hand]=poses[device].mDeviceToAbsoluteTracking;system_radial_pose_at[hand]=now;}
|
||||
const bool pose_recent=system_radial_pose_at[hand].time_since_epoch().count()&&now-system_radial_pose_at[hand]<std::chrono::milliseconds(750);
|
||||
bool valid=device!=vr::k_unTrackedDeviceIndexInvalid&&poses[0].bPoseIsValid&&hands[hand];
|
||||
bool down=false;
|
||||
char model[256]{};
|
||||
if(valid){
|
||||
vr_system->GetStringTrackedDeviceProperty(device,vr::Prop_RenderModelName_String,model,sizeof(model));
|
||||
vr::RenderModel_ControllerMode_State_t mode{};
|
||||
vr::RenderModel_ComponentState_t state{};
|
||||
bool component=vr::VRRenderModels()->GetComponentStateForDevicePath(model,"button_system",hands[hand],&mode,&state);
|
||||
down=component&&(state.uProperties&vr::VRComponentProperty_IsPressed);
|
||||
// Another overlay (notably the Frame keyboard) can own the active input
|
||||
// action while its panel is visible. The legacy controller state still
|
||||
// reports the physical button, so use it if the action-path state is idle.
|
||||
if(!down){
|
||||
vr::VRControllerState_t controller{};
|
||||
if(vr_system->GetControllerState(device,&controller,sizeof(controller))){
|
||||
vr::RenderModel_ComponentState_t raw{};
|
||||
down=bool(controller.ulButtonPressed&vr::ButtonMaskFromId(vr::k_EButton_System));
|
||||
if(!down&&vr::VRRenderModels()->GetComponentState(model,"button_system",&controller,&mode,&raw))down=bool(raw.uProperties&vr::VRComponentProperty_IsPressed);
|
||||
}
|
||||
}
|
||||
valid=component||down;
|
||||
}
|
||||
float right=0,up=0;
|
||||
if(valid&&system_radial.active&&pose_recent){
|
||||
const auto& point=system_radial_pose[hand];
|
||||
if(!system_radial_origin_valid){for(int i=0;i<3;i++)system_radial_origin.v[i]=point.m[i][3];system_radial_origin_valid=true;}
|
||||
for(int i=0;i<3;i++){
|
||||
float delta=point.m[i][3]-system_radial_origin.v[i];
|
||||
right+=delta*system_radial_right.v[i];
|
||||
up+=delta*system_radial_up.v[i];
|
||||
}
|
||||
}
|
||||
auto& view=*views.at("framely.system.radial");
|
||||
std::optional<SystemRadialDirection> hover;
|
||||
if(system_radial.open&&view.pointer_valid&&overlays->IsHoverTargetOverlay(view.overlay))hover=SystemRadial::hit(view.pointer.x,view.pointer.y);
|
||||
if(system_radial.open&&!pose_recent&&!hover&&valid)hover=system_radial_stick(model,hands[hand]);
|
||||
if(system_radial.open&&!pose_recent&&!hover)hover=system_radial.direction;
|
||||
auto event=system_radial.update(hand,valid,down,right,up,now,hover);
|
||||
if(event==SystemRadialEvent::Idle)continue;
|
||||
if(event==SystemRadialEvent::Begin){
|
||||
show(view,false);
|
||||
show(progress_view,false);
|
||||
system_radial_laser(view.overlay,false);
|
||||
view.pointer_valid=false;
|
||||
system_radial_preview_prepared=false;
|
||||
system_radial_progress_visible=false;
|
||||
const auto& point=system_radial_pose[hand];
|
||||
const auto& head=poses[0].mDeviceToAbsoluteTracking;
|
||||
system_radial_origin_valid=pose_recent;
|
||||
for(int i=0;i<3;i++){
|
||||
if(pose_recent)system_radial_origin.v[i]=point.m[i][3];
|
||||
system_radial_right.v[i]=head.m[i][0];
|
||||
system_radial_up.v[i]=head.m[i][1];
|
||||
}
|
||||
vr::HmdMatrix34_t transform{{{1,0,0,0},{0,1,0,0},{0,0,1,-.38f}}};
|
||||
overlays->SetOverlayWidthInMeters(progress_view.overlay,.68f);
|
||||
overlays->SetOverlayTransformTrackedDeviceRelative(progress_view.overlay,vr::k_unTrackedDeviceIndex_Hmd,&transform);
|
||||
overlays->SetOverlayWidthInMeters(view.overlay,.68f);
|
||||
overlays->ClearOverlayTexture(view.overlay);
|
||||
system_radial_sent={};
|
||||
}
|
||||
if(event==SystemRadialEvent::Open){
|
||||
show(progress_view,false);
|
||||
system_radial_progress_visible=false;
|
||||
if(pose_recent){
|
||||
const auto& point=system_radial_pose[hand];
|
||||
const auto& head=poses[0].mDeviceToAbsoluteTracking;
|
||||
auto transform=head;
|
||||
for(int i=0;i<3;i++){
|
||||
if(!system_radial_origin_valid)system_radial_origin.v[i]=point.m[i][3];
|
||||
system_radial_right.v[i]=head.m[i][0];
|
||||
system_radial_up.v[i]=head.m[i][1];
|
||||
transform.m[i][3]=point.m[i][3]-head.m[i][2]*.22f;
|
||||
}
|
||||
system_radial_origin_valid=true;
|
||||
overlays->SetOverlayTransformAbsolute(view.overlay,vr::TrackingUniverseStanding,&transform);
|
||||
}else{
|
||||
vr::HmdMatrix34_t transform{{{1,0,0,0},{0,1,0,0},{0,0,1,-.55f}}};
|
||||
overlays->SetOverlayTransformTrackedDeviceRelative(view.overlay,vr::k_unTrackedDeviceIndex_Hmd,&transform);
|
||||
}
|
||||
show(view,true);
|
||||
system_radial_laser(view.overlay,true);
|
||||
}
|
||||
if(event==SystemRadialEvent::Change){
|
||||
if(system_radial.direction==SystemRadialDirection::Right)radial_screenshot(true,false);
|
||||
else if(screenshot_radial_held)radial_screenshot(false,false);
|
||||
}
|
||||
if(event==SystemRadialEvent::Cancel||event==SystemRadialEvent::Commit){
|
||||
const auto direction=system_radial.direction;
|
||||
if(event==SystemRadialEvent::Cancel||direction!=SystemRadialDirection::Right){
|
||||
std::lock_guard<std::mutex> lock(visibility_mutex);
|
||||
if(pending_screenshot_hotkey.empty()||pending_screenshot_hotkey=="radialPrepare")pending_screenshot_hotkey="radialCancel";
|
||||
}
|
||||
show(view,false);
|
||||
show(progress_view,false);
|
||||
system_radial_laser(view.overlay,false);
|
||||
overlays->ClearOverlayTexture(view.overlay);
|
||||
system_radial_progress_visible=false;
|
||||
if(screenshot_radial_held)radial_screenshot(false,event==SystemRadialEvent::Commit&&direction==SystemRadialDirection::Right);
|
||||
if(event==SystemRadialEvent::Commit){
|
||||
if(direction==SystemRadialDirection::Center){
|
||||
if(auto* chaperone=vr::VRChaperone())chaperone->ResetZeroPose(vr::TrackingUniverseStanding);
|
||||
}else if(direction==SystemRadialDirection::Up)toggle_entry(true);
|
||||
else if(direction==SystemRadialDirection::Down){std::lock_guard<std::mutex> lock(queue_mutex);queue.push_back({{"kind","manager.open"}});}
|
||||
else if(direction==SystemRadialDirection::Left){std::lock_guard<std::mutex> lock(visibility_mutex);pending_system_action="record:"+std::to_string(++system_action_sequence);}
|
||||
else if(direction==SystemRadialDirection::Right){
|
||||
// A fast move can reach the sector between two host polls.
|
||||
std::lock_guard<std::mutex> lock(visibility_mutex);
|
||||
if(pending_screenshot_hotkey.empty())pending_screenshot_hotkey="radialCapture";
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(system_radial.active&&!system_radial_preview_prepared&&system_radial.progress(now)>=.25f){
|
||||
std::lock_guard<std::mutex> lock(visibility_mutex);
|
||||
if(pending_screenshot_hotkey.empty()){pending_screenshot_hotkey="radialPrepare";system_radial_preview_prepared=true;}
|
||||
}
|
||||
if(system_radial.active&&!system_radial.open&&!system_radial_progress_visible&&system_radial.progress(now)>=.4f){
|
||||
show(progress_view,true);
|
||||
system_radial_progress_visible=true;
|
||||
}
|
||||
if(system_radial.active&&now-system_radial_sent>=std::chrono::milliseconds(33)){
|
||||
system_radial_sent=now;
|
||||
bool recording=false;{std::lock_guard<std::mutex> lock(queue_mutex);recording=recording_state.value("active",false);}
|
||||
json detail={{"progress",std::clamp((system_radial.progress(now)-.4f)/.6f,0.f,1.f)},{"open",system_radial.open},{"direction",radial_direction(system_radial.direction)},{"recording",recording}};
|
||||
for(auto* display:{views.at("framely.system.radial.progress").get(),views.at("framely.system.radial").get()}){
|
||||
auto* frame=display->browser->get_main_frame(display->browser);
|
||||
execute(frame,"window.dispatchEvent(new CustomEvent('framely.systemMenu',{detail:"+detail.dump()+"}))");
|
||||
frame->base.release(&frame->base);
|
||||
}
|
||||
}
|
||||
}
|
||||
static void entry_actions(vr::VROverlayHandle_t button,bool available){
|
||||
for(int hand=0;hand<2;hand++){framing_trigger_known[hand]=false;framing_trigger_down[hand]=false;}
|
||||
const bool pointer_available=available||recording_indicator.active();
|
||||
@@ -689,7 +1069,20 @@ static void entry_actions(vr::VROverlayHandle_t button,bool available){
|
||||
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<json> commands;{std::lock_guard<std::mutex> lock(queue_mutex);commands.swap(queue);}for(const auto& c:commands){try{auto kind=c.value("kind","");if(kind=="cast.framing.open"){auto key="framely.framing";if(views.count(key)&&views.at(key)->closing){std::lock_guard<std::mutex> lock(queue_mutex);queue.push_back(c);continue;}auto geometry=c.at("geometry");RecordingRegionPlane plane;auto view=geometry.at("view");if(!recording_region_plane(view,geometry.at("referenceTan"),0,0,1,plane))continue;auto& v=create_view(key,"Recording frame",origin+"/cast/framing",false,1600,1600);v.region_editor_id=geometry.at("id");v.region_geometry=geometry;update_framing_placement(v);menu_open=false;hide_launcher(false);}else if(kind=="cast.framing.reference"){auto found=views.find("framely.framing");if(found!=views.end()&&!found->second->closing&&found->second->region_editor_id==c.value("id",std::string{})){double cx=c.at("centerTanX"),cy=c.at("centerTanY");if(std::isfinite(cx)&&std::isfinite(cy)&&std::abs(cx)<=1.733&&std::abs(cy)<=1.733){found->second->region_geometry["view"]["centerTanX"]=cx;found->second->region_geometry["view"]["centerTanY"]=cy;}}}else if(kind=="cast.framing.close"){if(views.count("framely.framing")&&views.at("framely.framing")->region_editor_id==c.value("id",std::string{}))close_view(*views.at("framely.framing"));}else 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<std::string>());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<std::string>());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<std::mutex> 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"Line truncated
|
||||
static void commands(){std::deque<json> commands;{std::lock_guard<std::mutex> lock(queue_mutex);commands.swap(queue);}for(const auto& c:commands){try{auto kind=c.value("kind","");if(kind=="cast.screenshot.open"){
|
||||
auto key="framely.screenshot";
|
||||
if(views.count(key)&&views.at(key)->closing){std::lock_guard<std::mutex> lock(queue_mutex);queue.push_back(c);continue;}
|
||||
auto id=c.at("id").get<std::string>();bool spatial=c.value("spatial",false);
|
||||
if(spatial){auto geometry=c.at("geometry");RecordingRegionPlane plane;if(!recording_region_plane(geometry.at("view"),geometry.at("referenceTan"),0,0,1,plane))continue;auto& v=create_view(key,"Screenshot frame",origin+"/cast/screenshot",false,1600,1600);v.region_editor_id=id;v.region_geometry=geometry;update_framing_placement(v);}
|
||||
else{auto& v=create_view(key,"Screenshot frame",origin+"/cast/screenshot",false,1120,780,"",1.25f);v.region_editor_id=id;}
|
||||
menu_open=false;hide_launcher(false);
|
||||
}else if(kind=="cast.screenshot.close"){
|
||||
if(views.count("framely.screenshot")&&views.at("framely.screenshot")->region_editor_id==c.value("id",std::string{}))close_view(*views.at("framely.screenshot"));
|
||||
}else if(kind=="cast.screenshot.shortcutPreview.open"){
|
||||
if(screenshot_radial_held){auto key="framely.screenshot.preview";if(views.count(key)&&views.at(key)->closing){std::lock_guard<std::mutex> lock(queue_mutex);queue.push_back(c);continue;}double aspect=c.value("aspect",1.0);if(!std::isfinite(aspect)||aspect<.3||aspect>4)aspect=1.0;int width=960,height=std::clamp(int(960/aspect),240,960);auto& v=create_view(key,"Screenshot preview",origin+"/cast/screenshot/shortcut",false,width,height,"",.95f);overlays->SetOverlayInputMethod(v.overlay,vr::VROverlayInputMethod_None);}
|
||||
}else if(kind=="cast.screenshot.shortcutPreview.close"){
|
||||
if(auto preview=views.find("framely.screenshot.preview");preview!=views.end())close_view(*preview->second);
|
||||
}else if(kind=="cast.framing.open"){auto key="framely.framing";if(views.count(key)&&views.at(key)->closing){std::lock_guard<std::mutex> lock(queue_mutex);queue.push_back(c);continue;}auto geometry=c.at("geometry");RecordingRegionPlane plane;auto view=geometry.at("view");if(!recording_region_plane(view,geometry.at("referenceTan"),0,0,1,plane))continue;auto& v=create_view(key,"Recording frame",origin+"/cast/framing",false,1600,1600);v.region_editor_id=geometry.at("id");v.region_geometry=geometry;update_framing_placement(v);menu_open=false;hide_launcher(false);}else if(kind=="cast.framing.reference"){auto found=views.find("framely.framing");if(found!=views.end()&&!found->second->closing&&found->second->region_editor_id==c.value("id",std::string{})){double cx=c.at("centerTanX"),cy=c.at("centerTanY");if(std::isfinite(cx)&&std::isfinite(cy)&&std::abs(cx)<=1.733&&std::abs(cy)<=1.733){found->second->region_geometry["view"]["centerTanX"]=cx;found->second->region_geometry["view"]["centerTanY"]=cy;}}}else if(kind=="cast.framing.close"){if(views.count("framely.framing")&&views.at("framely.framing")->region_editor_id==c.value("id",std::string{}))close_view(*views.at("framely.framing"));}else 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<std::string>());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<std::string>());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<std::mutex> 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"))Line truncated
|
||||
}
|
||||
static void release_framing_pointer(View& v){
|
||||
if(!v.framing_left_down)return;v.framing_left_down=false;
|
||||
@@ -697,7 +1090,7 @@ static void release_framing_pointer(View& v){
|
||||
auto* frame=v.browser->get_main_frame(v.browser);execute(frame,"window.dispatchEvent(new Event('framely.pointerRelease'))");frame->base.release(&frame->base);
|
||||
}
|
||||
static void reconcile_framing_pointer(View& v){
|
||||
if(v.key!="framely.framing"||!v.framing_left_down)return;
|
||||
if((v.key!="framely.framing"&&!(v.key=="framely.screenshot"&&v.region_geometry.contains("view")))||!v.framing_left_down)return;
|
||||
bool known=true,held=false,found=false,owner=false;
|
||||
for(int hand=0;hand<2;hand++){
|
||||
auto device=vr_system->GetTrackedDeviceIndexForControllerRole(hand?vr::TrackedControllerRole_RightHand:vr::TrackedControllerRole_LeftHand);
|
||||
@@ -710,7 +1103,7 @@ static void reconcile_framing_pointer(View& v){
|
||||
}
|
||||
if((owner||found)&&known&&!held)release_framing_pointer(v);
|
||||
}
|
||||
static void overlay_input(View& v,const vr::VREvent_t& e){if(v.closing)return;if((v.key=="notifications"||v.key=="launcher"||v.key=="menu")&&!v.shown&&e.eventType!=vr::VREvent_OverlayClosed)return;if(v.key=="launcher"&&!launcher_open&&e.eventType!=vr::VREvent_OverlayClosed)return;if(v.key=="launcher"&&!launcher_accepts(e)&&(e.eventType==vr::VREvent_Modal_Cancel||e.eventType==vr::VREvent_ButtonPress||e.eventType==vr::VREvent_MouseButtonDown||e.eventType==vr::VREvent_MouseButtonUp))return;if(keyboard_event(e,v.overlay))return;if(menu_open&&v.key!="menu"&&e.eventType==vr::VREvent_MouseButtonDown&&!keyboard_open&&menu_button.can_cancel(std::chrono::steady_clock::now()))menu_open=false;laser_cursor(v.overlay,e);cef_mouse_event_t mouse{};mouse.x=int(e.data.mouse.x);mouse.y=v.height-int(e.data.mouse.y);if(e.eventType==vr::VREvent_MouseMove||e.eventType==vr::VREvent_MouseButtonDown||e.eventType==vr::VREvent_MouseButtonUp){v.pointer=mouse;v.pointer_valid=true;v.pointer_device=e.trackedDeviceIndex;v.pointer_at=std::chrono::steady_clock::now();}switch(e.eventType){case vr::VREvent_Modal_Cancel:if(v.key=="launcher"&&!keyboard_open&&!gaze_active&&!launcher_dock_interaction()&&!overlays->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"<<std::endl;}break;case vr::VREvent_OverlayClosed:if(v.key=="launcher")hide_launcher();else if(v.key!="menu"&&v.key!="notifications")close_view(v);break;case vr::VREvent_ButtonPress:if(e.data.controller.button==vr::k_EButton_ApplicationMenu){if(v.key=="launcher"){launcher_back();break;}auto* frame=v.browser->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=="framely.framing")release_framing_pointer(v);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=="framely.framing")mouse.modifiers=v.framing_left_down?EVENTFLAG_LEFT_MOUSE_BUTTON:0;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;if(v.key=="framely.framing"&&button==MBT_LEFT)v.framing_left_down=e.eventType==vr::VREvent_MouseButtonDown;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(v.key=="framely.framing")release_framing_pointer(v);if(!keyboard_open&&!v.dashboard&&v.key!="menu"&&v.key!="notifications"&&v.key!="launcher"&&v.key.rfind("framely.cast.",0)!=0&&v.key!="framely.apk.permission"&&v.key!="framely.framing")close_view(v);break;default:break;}}
|
||||
static void overlay_input(View& v,const vr::VREvent_t& e){if(v.closing)return;if(v.key=="framely.system.radial"&&e.eventType==vr::VREvent_FocusLeave)v.pointer_valid=false;if((v.key=="notifications"||v.key=="launcher"||v.key=="menu")&&!v.shown&&e.eventType!=vr::VREvent_OverlayClosed)return;if(v.key=="launcher"&&!launcher_open&&e.eventType!=vr::VREvent_OverlayClosed)return;if(v.key=="launcher"&&!launcher_accepts(e)&&(e.eventType==vr::VREvent_Modal_Cancel||e.eventType==vr::VREvent_ButtonPress||e.eventType==vr::VREvent_MouseButtonDown||e.eventType==vr::VREvent_MouseButtonUp))return;if(keyboard_event(e,v.overlay))return;if(menu_open&&v.key!="menu"&&e.eventType==vr::VREvent_MouseButtonDown&&!keyboard_open&&menu_button.can_cancel(std::chrono::steady_clock::now()))menu_open=false;laser_cursor(v.overlay,e);cef_mouse_event_t mouse{};mouse.x=int(e.data.mouse.x);mouse.y=v.height-int(e.data.mouse.y);if(e.eventType==vr::VREvent_MouseMove||e.eventType==vr::VREvent_MouseButtonDown||e.eventType==vr::VREvent_MouseButtonUp){v.pointer=mouse;v.pointer_valid=true;v.pointer_device=e.trackedDeviceIndex;v.pointer_at=std::chrono::steady_clock::now();}switch(e.eventType){case vr::VREvent_Modal_Cancel:if(v.key=="launcher"&&!keyboard_open&&!gaze_active&&!launcher_dock_interaction()&&!overlays->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"<<std::endl;}break;case vr::VREvent_OverlayClosed:if(v.key=="launcher")hide_launcher();else if(v.key!="menu"&&v.key!="notifications")close_view(v);break;case vr::VREvent_ButtonPress:if(e.data.controller.button==vr::k_EButton_ApplicationMenu){if(v.key=="launcher"){launcher_back();break;}auto* frame=v.browser->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=="framely.framing"||(v.key=="framely.screenshot"&&v.region_geometry.contains("view"))))release_framing_pointer(v);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=="framely.framing"||(v.key=="framely.screenshot"&&v.region_geometry.contains("view"))))mouse.modifiers=v.framing_left_down?EVENTFLAG_LEFT_MOUSE_BUTTON:0;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:{if(v.key!=frame_keyboard_window_key)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;if((v.key=="framely.framing"||(v.key=="framely.screenshot"&&v.region_geometry.contains("view")))&&button==MBT_LEFT)v.framing_left_down=e.eventType==vr::VREvent_MouseButtonDown;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((v.key=="framely.framing"||(v.key=="framely.screenshot"&&v.region_geometry.contains("view"))))release_framing_pointer(v);if(!keyboard_open&&!v.dashboard&&v.key!=frame_keyboard_window_key&&v.key!="menu"&&v.key!="notifications"&&v.key!="launcher"&&v.key!="framely.system.radial"&&v.key.rfind("framely.cast.",0)!=0&&v.key!="framely.apk.permission"&&v.key!="framely.framing"&&v.key!="framely.screenshot")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<argc;i++){std::string s=argv[i];if(s=="--url")initial=argv[++i];else if(s=="--port")port=std::stoi(argv[++i]);else if(s=="--runtime")runtime=argv[++i];}
|
||||
@@ -724,7 +1117,7 @@ int main(int argc,char** argv){
|
||||
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 "<<vr_error<<"\n";return 3;}overlays=vr::VROverlay();setup_entry_actions();
|
||||
vr_system=vr::VR_Init(&vr_error,vr::VRApplication_Overlay);if(vr_error){std::cerr<<"SteamVR renderer unavailable "<<vr_error<<"\n";return 3;}overlays=vr::VROverlay();setup_entry_actions();system_radial_available=std::getenv("FRAMELY_STEAM_MENU_ENABLED")&&std::strcmp(std::getenv("FRAMELY_STEAM_MENU_ENABLED"),"1")==0;
|
||||
xdisplay=XOpenDisplay(nullptr);if(!xdisplay){vr::VR_Shutdown();return 4;}int n=0;int attrs[]={GLX_DRAWABLE_TYPE,GLX_PBUFFER_BIT,GLX_RENDER_TYPE,GLX_RGBA_BIT,None};auto* configs=glXChooseFBConfig(xdisplay,DefaultScreen(xdisplay),attrs,&n);if(!configs||!n){vr::VR_Shutdown();return 4;}int pba[]={GLX_PBUFFER_WIDTH,16,GLX_PBUFFER_HEIGHT,16,None};glsurface=glXCreatePbuffer(xdisplay,configs[0],pba);glcontext=glXCreateNewContext(xdisplay,configs[0],GLX_RGBA_TYPE,nullptr,True);XFree(configs);if(!glcontext||!glXMakeContextCurrent(xdisplay,glsurface,glsurface,glcontext)){vr::VR_Shutdown();return 4;}
|
||||
cef_settings_t settings{};settings.size=sizeof(settings);settings.windowless_rendering_enabled=1;std::string locale;for(const char* key:{"LC_ALL","LC_MESSAGES","LANG"}){const char* value=std::getenv(key);if(value&&*value){locale=value;break;}}auto suffix=locale.find_first_of(".@");if(suffix!=std::string::npos)locale.erase(suffix);std::replace(locale.begin(),locale.end(),'_','-');if(locale=="C"||locale=="POSIX"||locale.empty())locale="en-US";str(settings.accept_language_list,locale+",en-US,en");str(settings.root_cache_path,runtime+"/cef-cache");str(settings.log_file,runtime+"/cef.log");if(!cef_initialize(&args,&settings,nullptr,nullptr)){vr::VR_Shutdown();return 5;}
|
||||
render_handler.api.get_view_rect=[](cef_render_handler_t*,cef_browser_t* b,cef_rect_t* r){auto* v=find_browser(b);*r={0,0,v?v->width:600,v?v->height:840};};
|
||||
@@ -748,11 +1141,12 @@ int main(int argc,char** argv){
|
||||
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<<e.what()<<"\n";running=0;}
|
||||
if(system_radial_available)try{create_view("framely.system.radial.progress","Framely radial progress",initial+"?view=radial-progress",false,720,720);create_view("framely.system.radial","Framely radial menu",initial+"?view=radial",false,720,720);}catch(const std::exception& e){std::cerr<<"Radial menu unavailable: "<<e.what()<<"\n";system_radial_available=false;}
|
||||
const char* steam_root=std::getenv("FRAMELY_STEAM_ROOT");if(!ui_audio.prepare(steam_root?steam_root:""))std::cerr<<"Steam UI sounds unavailable\n";
|
||||
std::thread poller([port]{int failures=0;json last_feedback;json last_gaze;bool gaze_loaded=false;while(running){try{auto data=poll_http(port);notification_badge_count.store(data.value("notificationBadge",0u));launcher_prioritize_ui.store(data.value("launcher",json::object()).value("prioritizeUi",true));std::lock_guard<std::mutex> lock(queue_mutex);recording_state=data.value("recording",json::object());recording_region_state=data.value("recordingRegion",json::object());framing_editor_state=data.value("framingEditor",json(nullptr));if(!recording_state.is_object())recording_state=json::object();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: "<<e.what()<<"\n";running=0;}}std::this_thread::sleep_for(std::chrono::milliseconds(100));}});
|
||||
std::thread poller([port]{int failures=0;json last_feedback;json last_gaze;bool gaze_loaded=false;while(running){try{auto data=poll_http(port);notification_badge_count.store(data.value("notificationBadge",0u));const auto launcher=data.value("launcher",json::object());launcher_prioritize_ui.store(launcher.value("prioritizeUi",true));system_radial_enabled.store(launcher.value("systemRadialMenu",true));std::lock_guard<std::mutex> lock(queue_mutex);recording_state=data.value("recording",json::object());recording_region_state=data.value("recordingRegion",json::object());framing_editor_state=data.value("framingEditor",json(nullptr));screenshot_editor_state=data.value("screenshotEditor",json(nullptr));screenshot_shortcut_preview=data.value("screenshotShortcutPreview",json(nullptr));if(!recording_state.is_object())recording_state=json::object();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: "<<e.what()<<"\n";running=0;}}std::this_thread::sleep_for(std::chrono::milliseconds(100));}});
|
||||
std::thread priority_worker([]{LauncherPriority priority;while(running){priority.update(launcher_priority_active.load());std::this_thread::sleep_for(std::chrono::milliseconds(100));}});
|
||||
DockUVState uv{};OverlayPlacement button_placement;bool icon_active=false,icon_hover=false;unsigned icon_badge=0;int icon_progress=0;vr::VROverlayHandle_t last_dock=0;bool was_visible=false;vr::HmdMatrix34_t saved_anchor{},saved_frame{};float saved_size=0;DockCurveEvidence saved_evidence{};auto button_haptic_at=std::chrono::steady_clock::time_point{};auto anchored_at=std::chrono::steady_clock::time_point{};auto sampled_at=std::chrono::steady_clock::time_point{};
|
||||
while(running){auto frame_start=std::chrono::steady_clock::now();cef_do_message_loop_work();commands();json recording,region,framing;{std::lock_guard<std::mutex> lock(queue_mutex);recording=recording_state;region=recording_region_state;framing=framing_editor_state;}if(auto editor=views.find("framely.framing");editor!=views.end()&&framing_editor_should_close(framing,editor->second->region_editor_id))close_view(*editor->second);recording_indicator.tick(overlays,recording.value("active",false)&&recording.value("indicatorVisible",true)?recording.value("id",std::string{}):std::string{},recording.value("elapsedSeconds",uint64_t(0)));if(auto editor=views.find("framely.framing");editor!=views.end())update_framing_placement(*editor->second);if(region.value("visible",false)&®ion.contains("view")){auto presentation=recording_region_presentation(region["view"]);recording_region_overlay.tick(overlays,region,presentation.distance,&presentation.view);}else recording_region_overlay.tick(overlays,region);vr::VREvent_t global{};while(vr_system->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);
|
||||
while(running){auto frame_start=std::chrono::steady_clock::now();cef_do_message_loop_work();commands();json recording,region,framing,screenshot_editor,screenshot_preview;{std::lock_guard<std::mutex> lock(queue_mutex);recording=recording_state;region=recording_region_state;framing=framing_editor_state;screenshot_editor=screenshot_editor_state;screenshot_preview=screenshot_shortcut_preview;}if(auto editor=views.find("framely.framing");editor!=views.end()&&framing_editor_should_close(framing,editor->second->region_editor_id))close_view(*editor->second);if(auto editor=views.find("framely.screenshot");editor!=views.end()&&framing_editor_should_close(screenshot_editor,editor->second->region_editor_id))close_view(*editor->second);recording_indicator.tick(overlays,recording.value("active",false)&&recording.value("indicatorVisible",true)?recording.value("id",std::string{}):std::string{},recording.value("elapsedSeconds",uint64_t(0)));if(auto editor=views.find("framely.framing");editor!=views.end())update_framing_placement(*editor->second);if(auto editor=views.find("framely.screenshot");editor!=views.end()&&editor->second->region_geometry.is_object()&&editor->second->region_geometry.contains("view"))update_framing_placement(*editor->second);if(screenshot_radial_held&&screenshot_preview.is_object()&&screenshot_preview.value("visible",false))screenshot_region_overlay.tick(overlays,screenshot_preview);else screenshot_region_overlay.destroy();if(region.value("visible",false)&®ion.contains("view")){auto presentation=recording_region_presentation(region["view"]);recording_region_overlay.tick(overlays,region,presentation.distance,&presentation.view);}else recording_region_overlay.tick(overlays,region);external_keyboard_tick(std::chrono::steady_clock::now());vr::VREvent_t global{};while(vr_system->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;
|
||||
@@ -768,7 +1162,7 @@ int main(int argc,char** argv){
|
||||
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"&&key.rfind("framely.cast.",0)!=0&&key!="framely.apk.permission")close_view(*v);}was_visible=visible;
|
||||
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!=frame_keyboard_window_key&&key!="menu"&&key!="notifications"&&key!="launcher"&&key!="framely.system.radial"&&key.rfind("framely.cast.",0)!=0&&key!="framely.apk.permission")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.
|
||||
@@ -816,6 +1210,7 @@ int main(int argc,char** argv){
|
||||
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);
|
||||
system_radial_tick();
|
||||
// Some dashboard runtimes do not emit Modal_Cancel for clicks on their own
|
||||
// overlays. Observe the same controller click edge as a fallback, without
|
||||
// consuming the click destined for the other surface.
|
||||
@@ -831,7 +1226,7 @@ int main(int argc,char** argv){
|
||||
if(menu_open&&!keyboard_open&&menu_button.can_cancel(std::chrono::steady_clock::now())&&(pressed&click)&&views.count("menu")&&!overlays->IsHoverTargetOverlay(views.at("menu")->overlay)&&!overlays->IsHoverTargetOverlay(button))menu_open=false;
|
||||
}
|
||||
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"&&other.rfind("framely.cast.",0)!=0&&other!="framely.apk.permission"&&!keyboard_open)close_view(*t);}}
|
||||
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!=frame_keyboard_window_key&&other!="menu"&&other!="notifications"&&other!="launcher"&&other!="framely.system.radial"&&other.rfind("framely.cast.",0)!=0&&other!="framely.apk.permission"&&!keyboard_open)close_view(*t);}}
|
||||
reconcile_framing_pointer(*v);
|
||||
while(overlays->PollNextOverlayEvent(v->overlay,&event,sizeof(event)))overlay_input(*v,event);
|
||||
if(!v->cast_frame.path.empty()){glXMakeContextCurrent(xdisplay,glsurface,glsurface,glcontext);if(v->cast_frame.update())v->dirty=true;}
|
||||
@@ -910,5 +1305,7 @@ int main(int argc,char** argv){
|
||||
}
|
||||
recording_indicator.destroy();
|
||||
recording_region_overlay.destroy();
|
||||
screenshot_region_overlay.destroy();
|
||||
if(external_keyboard.active){frame_keyboard_command("dashboardClose",json::object());external_keyboard={};}
|
||||
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()<deadline){cef_do_message_loop_work();bool all=true;for(auto&[key,v]:views)all&=v->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;
|
||||
}
|
||||
+402
-9
@@ -13,6 +13,27 @@ use std::{
|
||||
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
|
||||
static APK_STORAGE_API_GATE: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn apply_system_menu_binding(helper: &Path, enabled: bool) -> Result<bool> {
|
||||
ensure!(helper.is_file(), "缺少 Steam 键绑定助手");
|
||||
let mut command=Command::new("python3");
|
||||
command.arg(helper);
|
||||
if !enabled {command.arg("--restore");}
|
||||
let status=command.status().context("无法更新 Steam 键绑定")?;
|
||||
ensure!(status.success()||status.code()==Some(10),"Steam 键绑定更新失败:{status}");
|
||||
Ok(status.code()==Some(10))
|
||||
}
|
||||
|
||||
fn schedule_steamvr_restart() -> Result<()> {
|
||||
let unit=format!("framely-system-menu-{}",cast_random_key());
|
||||
let output=Command::new("systemd-run")
|
||||
.args(["--user","--quiet","--collect","--on-active=2s"])
|
||||
.arg(format!("--unit={unit}"))
|
||||
.args(["/usr/bin/systemctl","--user","restart","steamvr.service"])
|
||||
.output().context("无法安排 SteamVR 重启")?;
|
||||
ensure!(output.status.success(),"无法安排 SteamVR 重启:{}",String::from_utf8_lossy(&output.stderr).trim());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct FrameAccess {
|
||||
plugin: String,
|
||||
version: String,
|
||||
@@ -36,8 +57,10 @@ struct Agent {
|
||||
terminals: crate::terminal::Terminals,
|
||||
files: Arc<crate::manager_files::Files>,
|
||||
catalog_cache: Arc<Mutex<std::collections::BTreeMap<String, Value>>>,
|
||||
casting: Mutex<crate::cast::runtime::Casting>,
|
||||
casting: Arc<Mutex<crate::cast::runtime::Casting>>,
|
||||
screenshot: Arc<Mutex<ScreenshotState>>,
|
||||
recording_overlay_cache: Mutex<Value>,
|
||||
radial_last: Mutex<String>,
|
||||
watch_access: Mutex<crate::cast::watch_access::Access>,
|
||||
receivers: Mutex<crate::cast::receiver::Receivers>,
|
||||
}
|
||||
@@ -46,6 +69,53 @@ struct EventLog {
|
||||
sequence: u64,
|
||||
entries: VecDeque<Value>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct ScreenshotState {
|
||||
id: String,
|
||||
open: bool,
|
||||
closed: bool,
|
||||
settings: Option<crate::cast::settings::Settings>,
|
||||
geometry: Value,
|
||||
preview: Option<Vec<u8>>,
|
||||
preview_aspect: Option<f64>,
|
||||
hotkey_active: bool,
|
||||
hotkey_preview: Value,
|
||||
hotkey_settings: Option<crate::cast::settings::Settings>,
|
||||
hotkey_thumbnail: Option<Vec<u8>>,
|
||||
hotkey_warm_geometry: Option<(crate::cast::settings::Settings, Value)>,
|
||||
hotkey_warm_pending: Option<crate::cast::settings::Settings>,
|
||||
hotkey_warm_capture: Option<(crate::cast::settings::Settings, crate::cast::screenshot::PreparedScreen)>,
|
||||
hotkey_warm_capture_pending: Option<crate::cast::settings::Settings>,
|
||||
}
|
||||
impl ScreenshotState {
|
||||
fn status(&self) -> Value { json!({"id":self.id,"open":self.open}) }
|
||||
fn editor(&self) -> Result<Value> {
|
||||
ensure!(self.open,"截图取景窗口已关闭");
|
||||
Ok(json!({"id":self.id,"settings":self.settings,"geometry":self.geometry,"spatial":self.geometry.is_object(),"preview":if self.preview.is_some(){Some(format!("/cast/screenshot/preview/{}",self.id))}else{None},"previewAspect":self.preview_aspect}))
|
||||
}
|
||||
fn acknowledge(&mut self,id:&str) {if self.id==id {self.closed=true;self.open=false;self.preview=None;}}
|
||||
}
|
||||
fn capture_hotkey_image(
|
||||
settings: &crate::cast::settings::Settings,
|
||||
binaries: &Path,
|
||||
panel: Option<&Path>,
|
||||
state: &Arc<Mutex<ScreenshotState>>,
|
||||
) -> Result<image::DynamicImage> {
|
||||
let deadline=std::time::Instant::now()+Duration::from_millis(1400);
|
||||
let prepared=loop {
|
||||
let mut state=state.lock().unwrap();
|
||||
if state.hotkey_warm_capture.as_ref().is_some_and(|(old,_)|old==settings) {
|
||||
break state.hotkey_warm_capture.take().map(|(_,capture)|capture);
|
||||
}
|
||||
if state.hotkey_warm_capture_pending.as_ref()!=Some(settings)||std::time::Instant::now()>=deadline {break None;}
|
||||
drop(state);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
};
|
||||
if let Some(prepared)=prepared {
|
||||
if let Ok(image)=prepared.capture() {return Ok(image);}
|
||||
}
|
||||
crate::cast::screenshot::capture(settings,binaries,panel,None)
|
||||
}
|
||||
impl EventLog {
|
||||
fn append(&mut self, mut event: Value) {
|
||||
self.sequence += 1;
|
||||
@@ -76,6 +146,29 @@ fn push(q: &Mutex<VecDeque<Value>>, v: Value) {
|
||||
}
|
||||
q.push_back(v);
|
||||
}
|
||||
fn radial_record(casting: Arc<Mutex<crate::cast::runtime::Casting>>, socket: PathBuf) {
|
||||
std::thread::spawn(move || {
|
||||
let result=(||->Result<()> {
|
||||
let mut cast=casting.lock().unwrap();
|
||||
if cast.recording_status()["active"]==true {
|
||||
cast.stop_recording()?;
|
||||
} else {
|
||||
let status=ipc::call(&socket,"status",json!({}))?;
|
||||
ensure!(status["agreement"]["accepted"]==true,"请先同意用户协议和隐私声明");
|
||||
let settings:crate::cast::settings::Settings=serde_json::from_value(status["database"]["casting"].clone())?;
|
||||
let panel=if settings.source==crate::cast::settings::Source::Screen {
|
||||
let response=ipc::call(&socket,"cast.panel.start",json!({}))?;
|
||||
Some(PathBuf::from(response["socket"].as_str().context("Missing panel socket")?))
|
||||
}else{None};
|
||||
cast.record(&settings,panel.as_deref(),&crate::steam::home()?.join("Videos/framely"))?;
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
if let Err(error)=result {
|
||||
let _=ipc::call(&socket,"cast.notification",json!({"notification":{"id":format!("cast.radial.{}",cast_random_key()),"title":"录屏失败","body":format!("{error:#}"),"durationMs":5000,"actions":[]}}));
|
||||
}
|
||||
});
|
||||
}
|
||||
impl Agent {
|
||||
fn send_page(&self, r: Request, remote: bool, guest: bool, network_cookie: Option<String>) -> Result<()> {
|
||||
let data = fs::read(self.assets.join("index.html"))?;
|
||||
@@ -422,11 +515,15 @@ impl Agent {
|
||||
}
|
||||
if path == "/host/poll" {
|
||||
ensure!(!remote && self.native(&r), "Native authentication failed");
|
||||
let mut hotkey_event=String::new();
|
||||
let mut radial_action=String::new();
|
||||
if r.method() == &Method::Post {
|
||||
let mut bytes = Vec::new();
|
||||
r.as_reader().take(32769).read_to_end(&mut bytes)?;
|
||||
ensure!(bytes.len() <= 32768, "Visibility report too large");
|
||||
let views: Value = serde_json::from_slice(&bytes)?;
|
||||
hotkey_event=views["screenshotHotkey"].as_str().unwrap_or("").to_owned();
|
||||
radial_action=views["radialAction"].as_str().unwrap_or("").to_owned();
|
||||
// Empty acknowledgements are sent on ordinary native polls.
|
||||
// Do not wait for the casting lock while describe/start is
|
||||
// running: the native HTTP timeout is intentionally short.
|
||||
@@ -436,6 +533,9 @@ impl Agent {
|
||||
if let Some(id)=views["framingClosed"].as_str().filter(|id|!id.is_empty()) {
|
||||
self.casting.lock().unwrap().region.acknowledge_editor_closed(id);
|
||||
}
|
||||
if let Some(id)=views["screenshotClosed"].as_str().filter(|id|!id.is_empty()) {
|
||||
self.screenshot.lock().unwrap().acknowledge(id);
|
||||
}
|
||||
if views["views"].is_object() {
|
||||
self.core("host.ui.visibility",views["views"].clone())?;
|
||||
if let Some(ids)=views["closedCasts"].as_array(){ensure!(ids.len()<=32,"Too many closed casts");for id in ids{if let Some(id)=id.as_str(){let _=self.receivers.lock().unwrap().control(id,"stop",json!({}));}}}
|
||||
@@ -443,6 +543,10 @@ impl Agent {
|
||||
self.core("host.ui.visibility", views)?;
|
||||
}
|
||||
}
|
||||
if radial_action.starts_with("record:") && *self.radial_last.lock().unwrap()!=radial_action {
|
||||
*self.radial_last.lock().unwrap()=radial_action.clone();
|
||||
radial_record(Arc::clone(&self.casting),self.socket.clone());
|
||||
}
|
||||
let mut queue = self.commands.lock().unwrap();
|
||||
let mut commands = Vec::new();
|
||||
let mut size = 0;
|
||||
@@ -463,9 +567,149 @@ impl Agent {
|
||||
.ok()
|
||||
.map(|v| v["database"].clone())
|
||||
.unwrap_or(json!({}));
|
||||
if !hotkey_event.is_empty() {let event=hotkey_event.as_str();
|
||||
match event {
|
||||
"radialPrepare" => {
|
||||
let settings:Result<crate::cast::settings::Settings,_>=serde_json::from_value(database["casting"].clone());
|
||||
if let Ok(settings)=settings {
|
||||
if settings.source==crate::cast::settings::Source::Screen {
|
||||
let mut state=self.screenshot.lock().unwrap();
|
||||
let ready=state.hotkey_warm_capture.as_ref().is_some_and(|(old,_)|old==&settings);
|
||||
let pending=state.hotkey_warm_capture_pending.as_ref()==Some(&settings);
|
||||
if !ready&&!pending {
|
||||
state.hotkey_warm_capture=None;
|
||||
state.hotkey_warm_capture_pending=Some(settings.clone());
|
||||
drop(state);
|
||||
let panel=self.core("cast.panel.start",json!({})).ok().and_then(|value|value["socket"].as_str().map(PathBuf::from));
|
||||
let binaries=self.casting.lock().unwrap().binaries.clone();
|
||||
let state=Arc::clone(&self.screenshot);
|
||||
let capture_settings=settings.clone();
|
||||
std::thread::spawn(move||{
|
||||
let capture=panel.as_deref().and_then(|path|crate::cast::screenshot::PreparedScreen::start(&capture_settings,&binaries,path).ok());
|
||||
let mut state=state.lock().unwrap();
|
||||
if state.hotkey_warm_capture_pending.as_ref()==Some(&capture_settings) {
|
||||
state.hotkey_warm_capture_pending=None;
|
||||
state.hotkey_warm_capture=capture.map(|capture|(capture_settings,capture));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if crate::cast::region::supported(&settings) {
|
||||
let mut state=self.screenshot.lock().unwrap();
|
||||
let pending=state.hotkey_warm_pending.as_ref()==Some(&settings);
|
||||
if !pending {
|
||||
state.hotkey_warm_geometry=None;
|
||||
state.hotkey_warm_pending=Some(settings.clone());
|
||||
drop(state);
|
||||
let state=Arc::clone(&self.screenshot);
|
||||
let binaries=self.casting.lock().unwrap().binaries.clone();
|
||||
std::thread::spawn(move||{
|
||||
let mut region=crate::cast::region::Region::default();region.editing=true;
|
||||
let geometry=region.refresh(&settings,&binaries,None).and_then(|_|region.editor());
|
||||
let mut state=state.lock().unwrap();
|
||||
if state.hotkey_warm_pending.as_ref()==Some(&settings) {
|
||||
state.hotkey_warm_pending=None;
|
||||
if let Ok(geometry)=geometry {
|
||||
if state.hotkey_active&&state.hotkey_settings.as_ref()==Some(&settings) {
|
||||
state.hotkey_preview=json!({"visible":true,"view":geometry["view"],"settings":settings});
|
||||
}
|
||||
state.hotkey_warm_geometry=Some((settings,geometry));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"radialCancel" => {
|
||||
let mut state=self.screenshot.lock().unwrap();
|
||||
state.hotkey_warm_capture_pending=None;
|
||||
state.hotkey_warm_capture=None;
|
||||
},
|
||||
"radialPress" | "radialCapture" => {
|
||||
let settings:Result<crate::cast::settings::Settings,_>=serde_json::from_value(database["casting"].clone());
|
||||
if let Ok(settings)=settings {
|
||||
let mut state=self.screenshot.lock().unwrap();
|
||||
if !state.open&&!state.hotkey_active {
|
||||
state.hotkey_active=true;state.hotkey_settings=Some(settings.clone());state.hotkey_preview=Value::Null;state.hotkey_thumbnail=None;
|
||||
drop(state);
|
||||
if crate::cast::region::supported(&settings) {
|
||||
let warm={let state=self.screenshot.lock().unwrap();state.hotkey_warm_geometry.as_ref().filter(|(old,_)|old==&settings).map(|(_,geometry)|geometry.clone())};
|
||||
let warm_pending={self.screenshot.lock().unwrap().hotkey_warm_pending.as_ref()==Some(&settings)};
|
||||
if let Some(geometry)=warm {
|
||||
let mut state=self.screenshot.lock().unwrap();
|
||||
if state.hotkey_active {state.hotkey_preview=json!({"visible":true,"view":geometry["view"],"settings":settings});}
|
||||
} else if !warm_pending {
|
||||
let state=Arc::clone(&self.screenshot);
|
||||
let binaries=self.casting.lock().unwrap().binaries.clone();
|
||||
std::thread::spawn(move||{
|
||||
let mut region=crate::cast::region::Region::default();region.editing=true;
|
||||
if region.refresh(&settings,&binaries,None).is_ok() {
|
||||
if let Ok(geometry)=region.editor() {
|
||||
let mut state=state.lock().unwrap();
|
||||
if state.hotkey_active {state.hotkey_preview=json!({"visible":true,"view":geometry["view"],"settings":settings});}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let state=Arc::clone(&self.screenshot);
|
||||
let queue=Arc::clone(&self.commands);
|
||||
let binaries=self.casting.lock().unwrap().binaries.clone();
|
||||
let panel=if settings.source==crate::cast::settings::Source::Screen {self.core("cast.panel.start",json!({})).ok().and_then(|value|value["socket"].as_str().map(PathBuf::from))}else{None};
|
||||
std::thread::spawn(move||{
|
||||
if let Ok(image)=crate::cast::screenshot::capture(&settings,&binaries,panel.as_deref(),None) {
|
||||
let mut bytes=std::io::Cursor::new(Vec::new());
|
||||
if image.thumbnail(960,540).write_to(&mut bytes,image::ImageFormat::Png).is_ok() {
|
||||
let mut state=state.lock().unwrap();
|
||||
if state.hotkey_active {state.hotkey_thumbnail=Some(bytes.into_inner());queue.lock().unwrap().push_back(json!({"kind":"cast.screenshot.shortcutPreview.open","aspect":image.width() as f64/image.height() as f64}));}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"release"|"cancel" => {
|
||||
let settings={let mut state=self.screenshot.lock().unwrap();state.hotkey_preview=Value::Null;state.hotkey_thumbnail=None;let active=state.hotkey_active;state.hotkey_active=false;if event=="cancel" {state.hotkey_warm_capture_pending=None;state.hotkey_warm_capture=None;}if active&&event=="release" {state.hotkey_settings.take()}else{state.hotkey_settings=None;None}};
|
||||
self.commands.lock().unwrap().push_back(json!({"kind":"cast.screenshot.shortcutPreview.close"}));
|
||||
if let Some(settings)=settings {
|
||||
let binaries=self.casting.lock().unwrap().binaries.clone();
|
||||
let control=self.socket.clone();
|
||||
let queue=Arc::clone(&self.commands);
|
||||
let state=Arc::clone(&self.screenshot);
|
||||
let panel=if settings.source==crate::cast::settings::Source::Screen {self.core("cast.panel.start",json!({})).ok().and_then(|value|value["socket"].as_str().map(PathBuf::from))}else{None};
|
||||
std::thread::spawn(move||{
|
||||
let result=(||->Result<PathBuf>{let image=capture_hotkey_image(&settings,&binaries,panel.as_deref(),&state)?;crate::cast::screenshot::save(&image,&crate::steam::home()?.join("Pictures/framely"))})();
|
||||
if result.is_ok(){push(&queue,json!({"kind":"cast.screenshot.sound"}));}
|
||||
let (title,body)=match result {Ok(path)=>("截图已保存",path.display().to_string()),Err(error)=>("截图失败",format!("{error:#}"))};
|
||||
let _=ipc::call(&control,"cast.notification",json!({"notification":{"id":format!("cast.screenshot.{}",cast_random_key()),"title":title,"body":body,"durationMs":5000,"actions":[]}}));
|
||||
});
|
||||
}
|
||||
},
|
||||
_=>{}
|
||||
}
|
||||
if event=="radialCapture" {
|
||||
// Selection and release may happen before the next native poll.
|
||||
let settings={let mut state=self.screenshot.lock().unwrap();state.hotkey_preview=Value::Null;state.hotkey_thumbnail=None;state.hotkey_active=false;state.hotkey_settings.take()};
|
||||
if let Some(settings)=settings {
|
||||
let binaries=self.casting.lock().unwrap().binaries.clone();
|
||||
let control=self.socket.clone();
|
||||
let queue=Arc::clone(&self.commands);
|
||||
let state=Arc::clone(&self.screenshot);
|
||||
let panel=if settings.source==crate::cast::settings::Source::Screen {self.core("cast.panel.start",json!({})).ok().and_then(|value|value["socket"].as_str().map(PathBuf::from))}else{None};
|
||||
std::thread::spawn(move||{
|
||||
let result=(||->Result<PathBuf>{let image=capture_hotkey_image(&settings,&binaries,panel.as_deref(),&state)?;crate::cast::screenshot::save(&image,&crate::steam::home()?.join("Pictures/framely"))})();
|
||||
if result.is_ok(){push(&queue,json!({"kind":"cast.screenshot.sound"}));}
|
||||
let (title,body)=match result {Ok(path)=>("截图已保存",path.display().to_string()),Err(error)=>("截图失败",format!("{error:#}"))};
|
||||
let _=ipc::call(&control,"cast.notification",json!({"notification":{"id":format!("cast.screenshot.{}",cast_random_key()),"title":title,"body":body,"durationMs":5000,"actions":[]}}));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return send_json(
|
||||
r,
|
||||
{let (mut recording,region,framing)=if let Ok(cast)=self.casting.try_lock() {let status=cast.recording_status();*self.recording_overlay_cache.lock().unwrap()=status.clone();(status,cast.region_status(),cast.region.editor_state())}else{(self.recording_overlay_cache.lock().unwrap().clone(),json!({"visible":false}),Value::Null)};recording["indicatorVisible"]=json!(database["casting"]["recordingIndicatorVisible"].as_bool().unwrap_or(true));json!({"feedback":database["feedback"],"commands":commands,"notificationBadge":badge,"gazeCalibration":self.core("launcher.gaze.calibration.get",json!({})).unwrap_or(Value::Null),"launcher":database["launcher"],"recording":recording,"recordingRegion":region,"framingEditor":framing})},
|
||||
{let (mut recording,region,framing)=if let Ok(cast)=self.casting.try_lock() {let status=cast.recording_status();*self.recording_overlay_cache.lock().unwrap()=status.clone();(status,cast.region_status(),cast.region.editor_state())}else{(self.recording_overlay_cache.lock().unwrap().clone(),json!({"visible":false}),Value::Null)};recording["indicatorVisible"]=json!(database["casting"]["recordingIndicatorVisible"].as_bool().unwrap_or(true));let screenshot=self.screenshot.lock().unwrap();json!({"feedback":database["feedback"],"commands":commands,"notificationBadge":badge,"gazeCalibration":self.core("launcher.gaze.calibration.get",json!({})).unwrap_or(Value::Null),"launcher":database["launcher"],"recording":recording,"recordingRegion":region,"framingEditor":framing,"screenshotEditor":screenshot.status(),"screenshotShortcutPreview":screenshot.hotkey_preview})},
|
||||
);
|
||||
}
|
||||
if let Some(rel) = path.strip_prefix("/plugin-assets/") {
|
||||
@@ -605,7 +849,11 @@ impl Agent {
|
||||
))
|
||||
.with_header(header(
|
||||
"Location",
|
||||
if r.url().ends_with("?view=manager") {
|
||||
if r.url().ends_with("?view=radial") {
|
||||
"/system/radial"
|
||||
} else if r.url().ends_with("?view=radial-progress") {
|
||||
"/system/radial/progress"
|
||||
} else if r.url().ends_with("?view=manager") {
|
||||
"/manager"
|
||||
} else if r.url().ends_with("?view=notifications") {
|
||||
"/notifications"
|
||||
@@ -965,9 +1213,32 @@ impl Agent {
|
||||
};
|
||||
return send_json(r, v);
|
||||
}
|
||||
if let Some(id)=path.strip_prefix("/cast/screenshot/preview/") {
|
||||
ensure!(r.method()==&Method::Get,"Invalid screenshot preview method");
|
||||
let state=self.screenshot.lock().unwrap();
|
||||
ensure!(state.open&&state.id==id,"截图预览已失效");
|
||||
let bytes=state.preview.as_ref().context("截图预览不可用")?.clone();
|
||||
r.respond(Response::from_data(bytes).with_header(header("Content-Type","image/png")).with_header(header("Cache-Control","no-store")))?;
|
||||
return Ok(());
|
||||
}
|
||||
if path == "/cast/screenshot/shortcut/preview.png" {
|
||||
let state=self.screenshot.lock().unwrap();
|
||||
ensure!(state.hotkey_active,"截图预览已失效");
|
||||
let bytes=state.hotkey_thumbnail.as_ref().context("截图预览不可用")?.clone();
|
||||
r.respond(Response::from_data(bytes).with_header(header("Content-Type","image/png")).with_header(header("Cache-Control","no-store")))?;
|
||||
return Ok(());
|
||||
}
|
||||
if path == "/cast/screenshot/shortcut" {
|
||||
r.respond(Response::from_string("<!doctype html><html><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><style>html,body{margin:0;width:100%;height:100%;background:#080b0e}body{display:grid;place-items:center}img{display:block;max-width:100%;max-height:100%;object-fit:contain}</style><img src='/cast/screenshot/shortcut/preview.png' alt='截图区域'></html>")
|
||||
.with_header(header("Content-Type","text/html; charset=utf-8")).with_header(header("Cache-Control","no-store")))?;
|
||||
return Ok(());
|
||||
}
|
||||
if path == "/"
|
||||
|| path == "/system/radial"
|
||||
|| path == "/system/radial/progress"
|
||||
|| path == "/cast/watch"
|
||||
|| path == "/cast/framing"
|
||||
|| path == "/cast/screenshot"
|
||||
|| path.starts_with("/cast/window/")
|
||||
|| path == "/apk/permission"
|
||||
|| path == "/manager"
|
||||
@@ -1077,6 +1348,85 @@ impl Agent {
|
||||
if let Err(error)=self.core("cast.region.visible.save",json!({"enabled":enabled})) {let _=cast.refresh_region(&previous);return Err(error);}
|
||||
Ok(json!({"enabled":enabled}))
|
||||
},
|
||||
"cast.screenshot.open" => {
|
||||
let settings:crate::cast::settings::Settings=serde_json::from_value(self.core("status",json!({}))?["database"]["casting"].clone())?;
|
||||
{let state=self.screenshot.lock().unwrap();ensure!(!state.open&&!state.hotkey_active,"请先完成当前截图");}
|
||||
let id=cast_random_key();
|
||||
let spatial=crate::cast::region::supported(&settings);
|
||||
let (geometry,preview,preview_aspect)=if spatial {
|
||||
let binaries=self.casting.lock().unwrap().binaries.clone();
|
||||
let mut region=crate::cast::region::Region::default();
|
||||
region.editing=true;region.editor_id=id.clone();region.refresh(&settings,&binaries,None)?;
|
||||
(region.editor()?,None,None)
|
||||
} else {
|
||||
let panel=if settings.source==crate::cast::settings::Source::Screen {
|
||||
Some(PathBuf::from(self.core("cast.panel.start",json!({}))?["socket"].as_str().context("Missing panel socket")?))
|
||||
}else{None};
|
||||
let binaries=self.casting.lock().unwrap().binaries.clone();
|
||||
let image=crate::cast::screenshot::capture(&settings,&binaries,panel.as_deref(),None)?;
|
||||
let mut bytes=std::io::Cursor::new(Vec::new());
|
||||
image.thumbnail(960,960).write_to(&mut bytes,image::ImageFormat::Png)?;
|
||||
(Value::Null,Some(bytes.into_inner()),Some(image.width() as f64/image.height() as f64))
|
||||
};
|
||||
let mut state=self.screenshot.lock().unwrap();
|
||||
ensure!(!state.open&&!state.hotkey_active,"请先完成当前截图");
|
||||
state.id=id.clone();state.open=true;state.closed=false;state.settings=Some(settings);state.geometry=geometry.clone();state.preview=preview;
|
||||
state.preview_aspect=preview_aspect;
|
||||
self.commands.lock().unwrap().push_back(json!({"kind":"cast.screenshot.open","id":id,"geometry":geometry,"spatial":spatial}));
|
||||
state.editor()
|
||||
},
|
||||
"cast.screenshot.get" => self.screenshot.lock().unwrap().editor(),
|
||||
"cast.screenshot.cancel" => {
|
||||
let id=p["id"].as_str().context("Missing screenshot session")?;
|
||||
let mut state=self.screenshot.lock().unwrap();
|
||||
ensure!(state.open&&state.id==id,"截图取景窗口已关闭");
|
||||
state.open=false;state.preview=None;
|
||||
self.commands.lock().unwrap().push_back(json!({"kind":"cast.screenshot.close","id":id}));
|
||||
Ok(json!(true))
|
||||
},
|
||||
"cast.screenshot.capture" => {
|
||||
let id=p["id"].as_str().context("Missing screenshot session")?.to_owned();
|
||||
let settings={
|
||||
let mut state=self.screenshot.lock().unwrap();
|
||||
ensure!(state.open&&state.id==id,"截图取景窗口已关闭");
|
||||
let mut settings=state.settings.clone().context("Missing screenshot settings")?;
|
||||
if crate::cast::region::supported(&settings) {
|
||||
let aspect=p["aspect"].as_f64().context("Missing screenshot aspect")?;
|
||||
ensure!(aspect.is_finite()&&(0.25..=4.).contains(&aspect),"截图比例无效");
|
||||
settings.width=1920;
|
||||
settings.height=(((1920.0/aspect).round() as u32).clamp(480,4096)+1)&!1;
|
||||
settings.horizontal_fov=Some(p["horizontalFov"].as_f64().context("Missing screenshot FOV")?);
|
||||
settings.center_x=p["centerX"].as_f64().context("Missing screenshot center")?;
|
||||
settings.center_y=p["centerY"].as_f64().context("Missing screenshot center")?;
|
||||
settings.validate()?;
|
||||
}
|
||||
state.open=false;state.preview=None;
|
||||
settings
|
||||
};
|
||||
self.commands.lock().unwrap().push_back(json!({"kind":"cast.screenshot.close","id":id}));
|
||||
let deadline=std::time::Instant::now()+Duration::from_secs(2);
|
||||
loop {
|
||||
if self.screenshot.lock().unwrap().closed {break;}
|
||||
ensure!(std::time::Instant::now()<deadline,"截图窗口未能及时关闭");
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
let crop=if crate::cast::region::supported(&settings){None}else{Some(serde_json::from_value(p["crop"].clone())?)};
|
||||
let panel=if settings.source==crate::cast::settings::Source::Screen {
|
||||
Some(PathBuf::from(self.core("cast.panel.start",json!({}))?["socket"].as_str().context("Missing panel socket")?))
|
||||
}else{None};
|
||||
let binaries=self.casting.lock().unwrap().binaries.clone();
|
||||
let result=(||->Result<Value>{
|
||||
let image=crate::cast::screenshot::capture(&settings,&binaries,panel.as_deref(),crop)?;
|
||||
let path=crate::cast::screenshot::save(&image,&crate::steam::home()?.join("Pictures/framely"))?;
|
||||
Ok(json!({"path":path,"width":image.width(),"height":image.height()}))
|
||||
})();
|
||||
if let Err(error)=&result {
|
||||
let _=self.core("cast.notification",json!({"notification":{"id":format!("cast.screenshot.{}",cast_random_key()),"title":"截图失败","body":error.to_string(),"durationMs":5000,"actions":[]}}));
|
||||
} else {
|
||||
push(&self.commands,json!({"kind":"cast.screenshot.sound"}));
|
||||
}
|
||||
result
|
||||
},
|
||||
"cast.framing.open" => {
|
||||
let settings:crate::cast::settings::Settings=serde_json::from_value(self.core("status",json!({}))?["database"]["casting"].clone())?;
|
||||
ensure!(crate::cast::region::supported(&settings),"画幅调整仅支持屏幕采集的单眼画面");
|
||||
@@ -1685,6 +2035,26 @@ impl Agent {
|
||||
}
|
||||
Ok(json!(true))
|
||||
}
|
||||
"launcher.settings.save" => {
|
||||
let next: LauncherSettings=serde_json::from_value(p.clone())?;
|
||||
next.validate()?;
|
||||
let previous: LauncherSettings=serde_json::from_value(self.core("status",json!({}))?["database"]["launcher"].clone())?;
|
||||
if next.system_radial_menu==previous.system_radial_menu {return self.core(method,p);}
|
||||
let helper=self.assets.parent().unwrap_or(&self.assets).join("input/steam-system-menu.py");
|
||||
let changed=apply_system_menu_binding(&helper,next.system_radial_menu)?;
|
||||
if let Err(error)=self.core(method,p) {
|
||||
if changed {let _=apply_system_menu_binding(&helper,previous.system_radial_menu);}
|
||||
return Err(error);
|
||||
}
|
||||
if changed {
|
||||
if let Err(error)=schedule_steamvr_restart() {
|
||||
let _=self.core(method,serde_json::to_value(&previous)?);
|
||||
let _=apply_system_menu_binding(&helper,previous.system_radial_menu);
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(json!(true))
|
||||
}
|
||||
"status"
|
||||
| "storage.plugin.status"
|
||||
| "storage.plugin.start"
|
||||
@@ -1706,7 +2076,6 @@ impl Agent {
|
||||
| "plugin.launch"
|
||||
| "plugin.launcher.action"
|
||||
| "feedback.settings.save"
|
||||
| "launcher.settings.save"
|
||||
| "launcher.gaze.calibration.get"
|
||||
| "launcher.gaze.calibration.save"
|
||||
| "launcher.order.save"
|
||||
@@ -2160,6 +2529,7 @@ pub fn serve(
|
||||
.context("Missing HTTP address")?
|
||||
.port();
|
||||
let origin = format!("http://127.0.0.1:{port}");
|
||||
let system_menu_binding = assets.parent().unwrap_or(&assets).join("input/steam-system-menu.py");
|
||||
let web_key = random_key();
|
||||
let native_key = random_key();
|
||||
let runtime = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
@@ -2174,8 +2544,10 @@ pub fn serve(
|
||||
token: ipc::call(&socket, "cast.watchAccess.get", json!({}))?["token"].as_str().map(str::to_owned),
|
||||
..Default::default()
|
||||
}),
|
||||
casting: Mutex::new(crate::cast::runtime::Casting::new(runtime.join("cast"), crate::cast::runtime::binary_directory(&assets))),
|
||||
recording_overlay_cache: Mutex::new(json!({"active":false})),
|
||||
casting: Arc::new(Mutex::new(crate::cast::runtime::Casting::new(runtime.join("cast"), crate::cast::runtime::binary_directory(&assets)))),
|
||||
screenshot: Arc::default(),
|
||||
recording_overlay_cache: Mutex::new(json!({"active":false})),
|
||||
radial_last: Mutex::new(String::new()),
|
||||
receivers: Mutex::new(crate::cast::receiver::Receivers::new(runtime.join("receive"), crate::cast::runtime::binary_directory(&assets))),
|
||||
socket,
|
||||
state,
|
||||
@@ -2308,8 +2680,24 @@ pub fn serve(
|
||||
if !http_only {
|
||||
let run = runtime.clone();
|
||||
let url = format!("{origin}/boot/{web_key}");
|
||||
let binding_socket=agent.socket.clone();
|
||||
std::thread::spawn(move || loop {
|
||||
wait_for_steam_session();
|
||||
let mut system_menu_ready = false;
|
||||
if system_menu_binding.exists() {
|
||||
let enabled=ipc::call(&binding_socket,"status",json!({})).ok()
|
||||
.and_then(|status|status["database"]["launcher"]["systemRadialMenu"].as_bool()).unwrap_or(true);
|
||||
match apply_system_menu_binding(&system_menu_binding,enabled) {
|
||||
Ok(true) => {
|
||||
if Command::new("systemctl").args(["--user", "restart", "steamvr.service"]).status().is_ok_and(|s|s.success()) {
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Ok(false) => system_menu_ready = enabled,
|
||||
Err(error) => eprintln!("SteamVR system menu binding failed: {error:#}"),
|
||||
}
|
||||
}
|
||||
let result = Command::new(&native)
|
||||
.arg("--url")
|
||||
.arg(&url)
|
||||
@@ -2326,6 +2714,7 @@ pub fn serve(
|
||||
"--change-stack-guard-on-fork=disable",
|
||||
])
|
||||
.env("FRAMELY_NATIVE_TOKEN", &native_key)
|
||||
.env("FRAMELY_STEAM_MENU_ENABLED", if system_menu_ready {"1"} else {"0"})
|
||||
.env("FRAMELY_STEAM_ROOT", {
|
||||
let home = PathBuf::from(std::env::var_os("HOME").unwrap_or_default());
|
||||
let steam = home.join(".local/share/Steam");
|
||||
@@ -2346,7 +2735,9 @@ pub fn serve(
|
||||
for r in server.incoming_requests() {
|
||||
let a = agent.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = a.handle(r);
|
||||
if let Err(error) = a.handle(r) {
|
||||
eprintln!("UI request failed: {error:#}");
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
@@ -2709,8 +3100,10 @@ mod tests {
|
||||
fn agent(server: &Server, root: &Path) -> Arc<Agent> {
|
||||
Arc::new(Agent {
|
||||
watch_access: Mutex::default(),
|
||||
casting: Mutex::new(crate::cast::runtime::Casting::new(root.join("cast"), root.join("media"))),
|
||||
recording_overlay_cache: Mutex::new(json!({"active":false})),
|
||||
casting: Arc::new(Mutex::new(crate::cast::runtime::Casting::new(root.join("cast"), root.join("media")))),
|
||||
screenshot: Arc::default(),
|
||||
recording_overlay_cache: Mutex::new(json!({"active":false})),
|
||||
radial_last: Mutex::new(String::new()),
|
||||
receivers: Mutex::new(crate::cast::receiver::Receivers::new(root.join("receive"), root.join("media"))),
|
||||
socket: root.join("control.sock"),
|
||||
state: root.join("state"),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {CastQuick} from './cast-quick';
|
||||
import {SystemRadialMenu} from './system-radial-menu';
|
||||
import {SpatialFraming} from './cast-spatial-framing';
|
||||
import {ScreenshotFraming} from './cast-screenshot';
|
||||
import {CastPlayer} from './cast-player';
|
||||
import {Casting,type CastSettings,type CastPage} from './casting';
|
||||
import {CastViewer} from './cast-viewer';
|
||||
@@ -39,7 +41,10 @@ const managerMode=location.pathname==='/manager';
|
||||
const empty:Status={version:'0.4.0',database:{plugins:{},sources:[],safeMode:false},notifications:[],running:[]};
|
||||
|
||||
function App(){
|
||||
if(location.pathname==='/system/radial')return <SystemRadialMenu/>;
|
||||
if(location.pathname==='/system/radial/progress')return <SystemRadialMenu progressOnly/>;
|
||||
if(location.pathname==='/cast/framing')return <SpatialFraming/>;
|
||||
if(location.pathname==='/cast/screenshot')return <ScreenshotFraming/>;
|
||||
if(location.pathname==='/apk/permission')return <ApkPermission/>;
|
||||
if(location.pathname==='/cast/watch')return <CastViewer/>;
|
||||
if(location.pathname.startsWith('/cast/window/'))return <CastPlayer/>;
|
||||
|
||||
Reference in new issue
Block a user