mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
* perf(epic): keep the hidden launcher from loading its store and Fab pages, and compress it while games run The hidden Epic Games Launcher that stays up for sign-in while a game runs pre-warmed its store and Fab pages on a display nobody sees. Under FEX each page used about one core and grew past a gigabyte, crashing and reloading in a loop. The hidden instance now skips the store pre-warm, gets a blank page in place of Fab, and is paged out to zRAM with the Steam client once a game has focus. The visible launcher is unchanged. * test(zram): keep the scan fixture PIDs above the kernel PID range
242 lines
13 KiB
C
242 lines
13 KiB
C
#if !defined(__x86_64__)
|
|
#error webkitinput is built for x86_64 guests only
|
|
#endif
|
|
|
|
typedef struct _WebKitSettings WebKitSettings;
|
|
typedef struct _WebKitUserContentManager WebKitUserContentManager;
|
|
typedef struct _WebKitUserScript WebKitUserScript;
|
|
|
|
extern void g_object_set(void *object, const char *first_property, ...) __attribute__((weak));
|
|
extern void *g_object_get_data(void *object, const char *key) __attribute__((weak));
|
|
extern void g_object_set_data(void *object, const char *key, void *data) __attribute__((weak));
|
|
extern void *dlsym(void *handle, const char *symbol) __attribute__((weak));
|
|
extern unsigned gtk_key_snooper_install(int (*snooper)(void *, void *, void *), void *data) __attribute__((weak));
|
|
extern void *gtk_widget_get_toplevel(void *widget) __attribute__((weak));
|
|
extern void gtk_container_forall(void *container, void (*callback)(void *, void *), void *data) __attribute__((weak));
|
|
extern unsigned long gtk_container_get_type(void) __attribute__((weak));
|
|
extern unsigned long webkit_web_view_get_type(void) __attribute__((weak));
|
|
extern int g_type_check_instance_is_a(void *instance, unsigned long type) __attribute__((weak));
|
|
extern const char *webkit_web_view_get_uri(void *view) __attribute__((weak));
|
|
extern void webkit_web_view_run_javascript(void *view, const char *script, void *cancellable, void *callback,
|
|
void *data) __attribute__((weak));
|
|
extern WebKitUserScript *webkit_user_script_new(const char *source, int frames, int time,
|
|
const char *const *allow, const char *const *block) __attribute__((weak));
|
|
extern void webkit_user_script_unref(WebKitUserScript *script) __attribute__((weak));
|
|
extern const char *g_getenv(const char *name) __attribute__((weak));
|
|
|
|
static const char NAV_SCRIPT[] =
|
|
"(()=>{let open=-1e9,arrowAt=-1e9,ringed=null,ringOutline='',ringOffset='',settleTimer=0,settling=false,redirecting=false;"
|
|
"const shown=()=>[...document.querySelectorAll('[role=dialog],[role=alertdialog],[aria-modal=true],[role=menu]')]"
|
|
".some(e=>e.getClientRects().length);"
|
|
"const hidden=e=>{const r=e.getBoundingClientRect();if(!r.width||!r.height)return true;"
|
|
"for(let n=e;n&&n.nodeType===1;n=n.parentElement){const s=getComputedStyle(n);"
|
|
"if(s.visibility==='hidden'||+s.opacity<0.1)return true;}return false;};"
|
|
"const windowButton=e=>/^(Minimize|Maximize|Restore|Close) window$/.test(e.getAttribute('aria-label')||'');"
|
|
"const editing=e=>!!e&&e.nodeType===1&&(e.isContentEditable||"
|
|
"e.matches('input:not([type=checkbox]):not([type=radio]):not([type=button]):not([type=submit]),textarea,select'));"
|
|
"const mainNav=()=>[...document.querySelectorAll('nav')].filter(n=>n.querySelectorAll('a[href]').length>1&&!hidden(n))"
|
|
".sort((a,b)=>a.getBoundingClientRect().y-b.getBoundingClientRect().y)[0];"
|
|
"const first=nav=>{const below=nav?nav.getBoundingClientRect().bottom:0,all=[];"
|
|
"for(const e of document.querySelectorAll('a[href],button,input,[role=button],[tabindex]')){"
|
|
"if(e.tabIndex<0||(nav&&nav.contains(e)))continue;const r=e.getBoundingClientRect();"
|
|
"if(r.top<below||r.bottom>innerHeight||windowButton(e)||hidden(e))continue;all.push({e,r});}"
|
|
"const big=all.filter(c=>c.r.width*c.r.height>=12000),list=big.length?big:all;"
|
|
"const key=c=>Math.round(c.r.top/8)*1e5+c.r.left;"
|
|
"list.sort((a,b)=>key(a)-key(b));return list.length?list[0].e:null;};"
|
|
"const big=e=>{const r=e.getBoundingClientRect();return r.width*r.height>=12000;};"
|
|
"const toward=(from,key,nav,small)=>{const r=from.getBoundingClientRect(),cx=r.left+r.width/2,cy=r.top+r.height/2;let best=null,score=1e9;"
|
|
"for(const c of document.querySelectorAll('a[href],button,[role=button],[tabindex]')){if(c===from||c.tabIndex<0||(nav&&nav.contains(c))||from.contains(c)||c.contains(from))continue;"
|
|
"const q=c.getBoundingClientRect();if(!small&&q.width*q.height<12000)continue;const qx=q.left+q.width/2,qy=q.top+q.height/2;let along,across;"
|
|
"if(key==='ArrowDown'){along=q.top-r.bottom;across=qx-cx;}else if(key==='ArrowUp'){along=r.top-q.bottom;across=qx-cx;}"
|
|
"else if(key==='ArrowRight'){along=q.left-r.right;across=qy-cy;}else{along=r.left-q.right;across=qy-cy;}"
|
|
"if(along<-4)continue;const d=Math.max(along,0)+2*Math.abs(across);if(d<score&&!windowButton(c)&&!hidden(c)){score=d;best=c;}}return best;};"
|
|
"const settle=()=>{const a=document.activeElement;if(editing(a)||shown())return;const nav=mainNav(),list=[];"
|
|
"for(const c of document.querySelectorAll('a[href],button,[role=button],[tabindex]')){if(c.tabIndex<0||(nav&&nav.contains(c)))continue;"
|
|
"const r=c.getBoundingClientRect();if(r.width*r.height>=12000&&r.top>=0&&r.left>=0&&r.bottom<=innerHeight&&r.right<=innerWidth)list.push({c,r});}"
|
|
"const key=x=>Math.round(x.r.top/8)*1e5+x.r.left;list.sort((x,y)=>key(x)-key(y));"
|
|
"for(const {c,r} of list){const hit=document.elementFromPoint(r.left+r.width/2,r.top+8);"
|
|
"if(hit&&(hit===c||c.contains(hit))&&!hidden(c)){arrowAt=performance.now();c.focus({preventScroll:true});return;}}};"
|
|
"const arm=()=>{clearTimeout(settleTimer);settleTimer=setTimeout(()=>{settling=false;settle();},200);};"
|
|
"addEventListener('scroll',()=>{if(settling)arm();},true);"
|
|
"const unring=()=>{if(ringed){ringed.style.outline=ringOutline;ringed.style.outlineOffset=ringOffset;ringed=null;}};"
|
|
"addEventListener('keydown',e=>{const k=e.key||'';if(k.startsWith('Arrow')){arrowAt=e.timeStamp;const a=document.activeElement;"
|
|
"if(!editing(a)&&!shown()){const nav=mainNav();"
|
|
"if(!a||a===document.body||hidden(a)||(k==='ArrowDown'&&nav&&nav.contains(a))){const f=first(nav);"
|
|
"if(f){e.preventDefault();e.stopImmediatePropagation();f.focus();f.scrollIntoView({block:'nearest'});}}"
|
|
"else if(big(a)&&!e.ctrlKey&&!e.altKey){let t=toward(a,k,nav);"
|
|
"if(!t&&(k==='ArrowUp'||k==='ArrowDown'))t=toward(a,k,nav,true);"
|
|
"if(t){e.preventDefault();e.stopImmediatePropagation();t.focus({preventScroll:true});t.scrollIntoView({block:'nearest',inline:'nearest'});}}}}"
|
|
"if(k==='Escape')open=shown()?e.timeStamp:-1e9;"
|
|
"if(e.code==='F13'){e.preventDefault();e.stopImmediatePropagation();settling=true;arm();}},true);"
|
|
"addEventListener('keydown',e=>{if(e.key!=='BrowserBack')return;e.preventDefault();e.stopImmediatePropagation();"
|
|
"const a=document.activeElement;if(editing(a)){a.blur();return;}"
|
|
"if(e.timeStamp-open>500)history.back();},true);"
|
|
"window.droiddeckTab=step=>{"
|
|
"const nav=mainNav();if(!nav)return;"
|
|
"const tabs=[...nav.querySelectorAll('a[href]')].filter(a=>!hidden(a)),part=p=>p.split('/')[1]||'';"
|
|
"const at=tabs.findIndex(a=>part(new URL(a.href).pathname)===part(location.pathname));"
|
|
"const to=tabs[Math.min(tabs.length-1,Math.max(0,at+step))];"
|
|
"if(at>=0&&to!==tabs[at]){to.click();to.blur();}};"
|
|
"const redirect=n=>{redirecting=true;try{n.focus();}finally{redirecting=false;}};"
|
|
"addEventListener('focusout',unring,true);"
|
|
"addEventListener('focusin',e=>{const t=e.target;if(!(t instanceof Element)||e.timeStamp-arrowAt>1000)return;"
|
|
"const back=e.relatedTarget instanceof Element&&e.relatedTarget.focus?e.relatedTarget:null;"
|
|
"if(!redirecting&&windowButton(t)){if(back)redirect(back);return;}"
|
|
"if(!redirecting&&hidden(t)){let a=t.parentElement;"
|
|
"while(a&&!(a.matches('a[href],button,[role=button],[tabindex]')&&!hidden(a)))a=a.parentElement;"
|
|
"if(a)redirect(a);else if(back)redirect(back);return;}"
|
|
"unring();ringed=(t.matches('input,textarea')&&t.closest('label'))||t;"
|
|
"ringOutline=ringed.style.outline;ringOffset=ringed.style.outlineOffset;"
|
|
"ringed.style.outline='3px solid #fff';ringed.style.outlineOffset='2px';},true);})();";
|
|
|
|
typedef struct {
|
|
int type;
|
|
void *window;
|
|
signed char send_event;
|
|
unsigned time;
|
|
unsigned state;
|
|
unsigned keyval;
|
|
} KeyEvent;
|
|
|
|
static void *launcher_view;
|
|
|
|
static void find_launcher_view(void *widget, void *data)
|
|
{
|
|
(void)data;
|
|
if (launcher_view)
|
|
return;
|
|
if (g_type_check_instance_is_a(widget, webkit_web_view_get_type())) {
|
|
const char *uri = webkit_web_view_get_uri(widget);
|
|
const char *prefix = "tauri:";
|
|
while (uri && *prefix && *uri == *prefix) {
|
|
uri++;
|
|
prefix++;
|
|
}
|
|
if (uri && !*prefix)
|
|
launcher_view = widget;
|
|
} else if (g_type_check_instance_is_a(widget, gtk_container_get_type())) {
|
|
gtk_container_forall(widget, find_launcher_view, 0);
|
|
}
|
|
}
|
|
|
|
static int switch_tab(void *grab, void *event_data, void *data)
|
|
{
|
|
KeyEvent *event = event_data;
|
|
(void)data;
|
|
if (!grab || event->type != 8 || !(event->state & 4) || (event->keyval != 0xff55 && event->keyval != 0xff56))
|
|
return 0;
|
|
launcher_view = 0;
|
|
find_launcher_view(gtk_widget_get_toplevel(grab), 0);
|
|
if (!launcher_view)
|
|
return 0;
|
|
webkit_web_view_run_javascript(launcher_view, event->keyval == 0xff56 ? "window.droiddeckTab&&droiddeckTab(1)"
|
|
: "window.droiddeckTab&&droiddeckTab(-1)",
|
|
0, 0, 0);
|
|
return 1;
|
|
}
|
|
|
|
static void watch_tab_keys(void)
|
|
{
|
|
static int watching;
|
|
if (watching || !gtk_key_snooper_install || !gtk_widget_get_toplevel || !gtk_container_forall ||
|
|
!gtk_container_get_type || !webkit_web_view_get_type || !g_type_check_instance_is_a ||
|
|
!webkit_web_view_get_uri || !webkit_web_view_run_javascript)
|
|
return;
|
|
watching = 1;
|
|
gtk_key_snooper_install(switch_tab, 0);
|
|
}
|
|
|
|
static void set(WebKitSettings *settings, const char *setter, const char *property, int value)
|
|
{
|
|
static int setting;
|
|
if (setting) {
|
|
void (*next)(WebKitSettings *, int) = dlsym ? (void (*)(WebKitSettings *, int))dlsym((void *)-1l, setter) : 0;
|
|
if (next)
|
|
next(settings, value);
|
|
return;
|
|
}
|
|
if (!g_object_set)
|
|
return;
|
|
setting = 1;
|
|
g_object_set(settings, property, value, "enable-smooth-scrolling", 0, "enable-spatial-navigation", 1, (const char *)0);
|
|
setting = 0;
|
|
}
|
|
|
|
#define SETTER(name, property) \
|
|
__attribute__((visibility("default"))) void webkit_settings_set_##name(WebKitSettings *settings, int value) \
|
|
{ \
|
|
set(settings, "webkit_settings_set_" #name, property, value); \
|
|
}
|
|
|
|
SETTER(enable_javascript, "enable-javascript")
|
|
SETTER(enable_developer_extras, "enable-developer-extras")
|
|
SETTER(enable_webgl, "enable-webgl")
|
|
SETTER(enable_webaudio, "enable-webaudio")
|
|
SETTER(enable_page_cache, "enable-page-cache")
|
|
SETTER(javascript_can_access_clipboard, "javascript-can-access-clipboard")
|
|
SETTER(javascript_can_open_windows_automatically, "javascript-can-open-windows-automatically")
|
|
SETTER(enable_back_forward_navigation_gestures, "enable-back-forward-navigation-gestures")
|
|
|
|
__attribute__((visibility("default"))) void webkit_user_content_manager_add_script(WebKitUserContentManager *manager,
|
|
WebKitUserScript *script)
|
|
{
|
|
static void (*next)(WebKitUserContentManager *, WebKitUserScript *);
|
|
if (!next && dlsym)
|
|
next = (void (*)(WebKitUserContentManager *, WebKitUserScript *))dlsym((void *)-1l, "webkit_user_content_manager_add_script");
|
|
if (!next)
|
|
return;
|
|
watch_tab_keys();
|
|
if (g_object_get_data && g_object_set_data && webkit_user_script_new && webkit_user_script_unref &&
|
|
!g_object_get_data(manager, "droiddeck-nav")) {
|
|
g_object_set_data(manager, "droiddeck-nav", (void *)1);
|
|
WebKitUserScript *nav = webkit_user_script_new(NAV_SCRIPT, 1, 0, 0, 0);
|
|
if (nav) {
|
|
next(manager, nav);
|
|
webkit_user_script_unref(nav);
|
|
}
|
|
}
|
|
next(manager, script);
|
|
}
|
|
|
|
static int starts(const char *text, const char *prefix)
|
|
{
|
|
while (*prefix)
|
|
if (*text++ != *prefix++)
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
static int fab_uri(const char *uri)
|
|
{
|
|
if (!uri)
|
|
return 0;
|
|
if (starts(uri, "https://"))
|
|
uri += 8;
|
|
else if (starts(uri, "http://"))
|
|
uri += 7;
|
|
else
|
|
return 0;
|
|
if (starts(uri, "www."))
|
|
uri += 4;
|
|
if (!starts(uri, "fab.com"))
|
|
return 0;
|
|
uri += 7;
|
|
return !*uri || *uri == '/' || *uri == '?' || *uri == '#' || *uri == ':';
|
|
}
|
|
|
|
static int hidden_launcher(void)
|
|
{
|
|
const char *hidden = g_getenv ? g_getenv("DROIDDECK_EPIC_HIDDEN") : 0;
|
|
return hidden && hidden[0] == '1' && !hidden[1];
|
|
}
|
|
|
|
__attribute__((visibility("default"))) void webkit_web_view_load_uri(void *view, const char *uri)
|
|
{
|
|
static void (*next)(void *, const char *);
|
|
if (!next && dlsym)
|
|
next = (void (*)(void *, const char *))dlsym((void *)-1l, "webkit_web_view_load_uri");
|
|
if (!next)
|
|
return;
|
|
next(view, hidden_launcher() && fab_uri(uri) ? "about:blank" : uri);
|
|
}
|