Enable distance-based UI showing. Add "show while holding CTRL" option to the in-player UI

This commit is contained in:
Tamius Han 2026-01-14 22:39:42 +01:00
parent f25749470b
commit f0baa6ad67
6 changed files with 120 additions and 37 deletions

View File

@ -1,3 +1,5 @@
import { InPlayerUIOptions } from '@src/common/interfaces/SettingsInterface';
export interface MenuItemConfig { export interface MenuItemConfig {
label: string; label: string;
subitems?: MenuItemConfig[]; subitems?: MenuItemConfig[];
@ -20,7 +22,7 @@ export enum MenuPosition {
export interface MenuConfig { export interface MenuConfig {
isGlobal?: boolean; isGlobal?: boolean;
ui: InPlayerUIOptions;
menuPosition: MenuPosition; menuPosition: MenuPosition;
activationRadius?: number;
items: MenuItemConfig[]; items: MenuItemConfig[];
} }

View File

@ -308,6 +308,23 @@ interface DevSettings {
loadFromSnapshot: boolean, loadFromSnapshot: boolean,
} }
export interface InPlayerUIOptions {
activatorAlignment: 'left' | 'right',
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width
minEnabledHeight: number, // don't show UI if player is narrower than % of screen height
activation: 'player' | 'player-ctrl' | 'trigger-zone' | 'distance' | 'none', // what needs to be hovered in order for UI to be visible
activationDistance: number,
activationDistanceUnits: '%' | 'px',
activatorPadding: 10,
activatorPaddingUnit: '%' | 'px',
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
};
interface SettingsInterface { interface SettingsInterface {
_updateFlags?: { _updateFlags?: {
requireReload?: SettingsReloadFlags, requireReload?: SettingsReloadFlags,
@ -319,18 +336,7 @@ interface SettingsInterface {
aard: AardSettings, aard: AardSettings,
ui: { ui: {
inPlayer: { inPlayer: InPlayerUIOptions,
popupAlignment: 'left' | 'right',
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width
minEnabledHeight: number, // don't show UI if player is narrower than % of screen height
activation: 'player' | 'trigger-zone' | 'distance' | 'none', // what needs to be hovered in order for UI to be visible
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
},
devMode?: boolean, devMode?: boolean,
dev: DevUiConfig, dev: DevUiConfig,
} }

View File

@ -264,7 +264,11 @@ const ExtensionConf: SettingsInterface = {
minEnabledWidth: 0.75, minEnabledWidth: 0.75,
minEnabledHeight: 0.75, minEnabledHeight: 0.75,
activation: 'player', activation: 'player',
popupAlignment: 'left', activationDistance: 100,
activationDistanceUnits: '%',
activatorAlignment: 'left',
activatorPadding: 10,
activatorPaddingUnit: '%',
triggerZoneDimensions: { triggerZoneDimensions: {
width: 0.5, width: 0.5,
height: 0.5, height: 0.5,

View File

@ -12,6 +12,7 @@ export class ClientMenu {
public get root(): HTMLDivElement { public get root(): HTMLDivElement {
return this._root; return this._root;
} }
private trigger: HTMLDivElement;
private visible = false; private visible = false;
private menuPositionClasses: string[] = []; private menuPositionClasses: string[] = [];
@ -22,6 +23,8 @@ export class ClientMenu {
private lastMouseMove = performance.now(); private lastMouseMove = performance.now();
private idleTimer?: number; private idleTimer?: number;
private onDocumentMouseMove?: (e: MouseEvent) => void; private onDocumentMouseMove?: (e: MouseEvent) => void;
private onDocumentMouseLeave?: () => void; private onDocumentMouseLeave?: () => void;
private idleIntervalId?: number; private idleIntervalId?: number;
@ -75,16 +78,19 @@ export class ClientMenu {
} }
private getActivationRadius(anchorEl: HTMLElement): number | null { private getActivationRadius(anchorEl: HTMLElement): number | null {
if (this.config.activationRadius == null) return null; if (this.config.ui.activation !== 'distance') {
return undefined;
}
if (typeof this.config.activationRadius === 'number') { if (this.config.ui.activationDistanceUnits === 'px') {
return this.config.activationRadius; return +this.config.ui.activationDistance;
} }
// percentage string // percentage string
const rect = anchorEl.getBoundingClientRect(); const rect = anchorEl.getBoundingClientRect();
const pct = parseFloat(this.config.activationRadius); const percent = +this.config.ui.activationDistance;
return Math.max(rect.width, rect.height) * (pct / 100);
return Math.max(rect.width, rect.height) * (percent / 100);
} }
private injectStyles() { private injectStyles() {
@ -154,8 +160,6 @@ export class ClientMenu {
pointerEvents: 'none', pointerEvents: 'none',
background: 'transparent', background: 'transparent',
}); });
console.log('UI host created:', this.host);
} }
private createShadow() { private createShadow() {
@ -208,7 +212,9 @@ export class ClientMenu {
const trigger = document.createElement('div'); const trigger = document.createElement('div');
trigger.classList = 'uw-menu-trigger uw-trigger'; trigger.classList = 'uw-menu-trigger uw-trigger';
trigger.style = `margin: ${this.config.ui.activatorPadding ?? 10} ${this.config.ui.activatorPaddingUnit ?? '%'}`;
trigger.textContent = 'Ultrawidify'; trigger.textContent = 'Ultrawidify';
this.trigger = trigger;
const submenu = this.buildSubmenu(this.config.items); const submenu = this.buildSubmenu(this.config.items);
@ -281,24 +287,38 @@ export class ClientMenu {
} }
private bindGlobalMouse(anchorEl: HTMLElement) { private bindGlobalMouse(anchorEl: HTMLElement) {
const rect = anchorEl.getBoundingClientRect(); const playerRect = anchorEl.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2; let menuActivatorRect, cx, cy;
const activationRadius = this.getActivationRadius(anchorEl); const activationRadius = this.getActivationRadius(anchorEl);
const recalculateActivator = () => {
menuActivatorRect = this.trigger.getBoundingClientRect();
cx = menuActivatorRect.left + menuActivatorRect.width / 2;
cy = menuActivatorRect.top + menuActivatorRect.height / 2;
}
recalculateActivator();
this.onDocumentMouseMove = (e: MouseEvent) => { this.onDocumentMouseMove = (e: MouseEvent) => {
this.lastMouseMove = performance.now(); this.lastMouseMove = performance.now();
if (activationRadius != null) { if (activationRadius != null) {
if (! menuActivatorRect.width) {
recalculateActivator();
}
const d = Math.hypot(e.clientX - cx, e.clientY - cy); const d = Math.hypot(e.clientX - cx, e.clientY - cy);
this.isWithinActivation = d <= activationRadius; this.isWithinActivation = d <= activationRadius;
} else { } else {
this.isWithinActivation = this.isWithinActivation =
e.clientX >= rect.left && e.clientX >= playerRect.left &&
e.clientX <= rect.right && e.clientX <= playerRect.right &&
e.clientY >= rect.top && e.clientY >= playerRect.top &&
e.clientY <= rect.bottom; e.clientY <= playerRect.bottom &&
(this.config.ui.activation !== 'player-ctrl' || e.ctrlKey);
} }
this.updateVisibility(); this.updateVisibility();
@ -319,7 +339,7 @@ export class ClientMenu {
private startIdleWatcher() { private startIdleWatcher() {
this.idleIntervalId = window.setInterval(() => { this.idleIntervalId = window.setInterval(() => {
const idle = performance.now() - this.lastMouseMove > 1000; const idle = performance.now() - this.lastMouseMove > 1000;
if (idle) { if (idle && !this.isHovered) {
this.hide(); this.hide();
} }
}, 200); }, 200);

View File

@ -184,7 +184,7 @@ class UI {
if (this.uiConfig.parentElement) { if (this.uiConfig.parentElement) {
const menuConfig = { const menuConfig = {
isGlobal: this.isGlobal, isGlobal: this.isGlobal,
menuPosition: MenuPosition.Left, ui: this.settings.active.ui.inPlayer,
items: [ items: [
{ {
customClassList: 'uw-site-info', customClassList: 'uw-site-info',

View File

@ -5,17 +5,32 @@
<!-- The rest of the tab is under 'edit ratios and shortcuts' row --> <!-- The rest of the tab is under 'edit ratios and shortcuts' row -->
<div v-if="settings" class="flex flex-col" style="width: 100%"> <div v-if="settings" class="flex flex-col" style="width: 100%">
<div class="flex flex-col compact-form"> <div class="flex flex-col gap-2">
<div <div
class="flex flex-col field-group compact-form" class="flex flex-col field-group compact-form gap-2"
> >
<div class="field disabled"> <div class="field">
<div class="label"> <div class="label">
Popup activator position: Popup activator position:
</div> </div>
<div class="select"> <div class="select">
<select <select
v-model="settings.active.ui.inPlayer.popupAlignment" v-model="settings.active.ui.inPlayer.activatorAlignment"
@change="saveSettings()"
>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</div>
</div>
<div class="field">
<div class="label">
Popup activator padding:
</div>
<div class="select">
<select
v-model="settings.active.ui.inPlayer.activatorAlignment"
@change="saveSettings()" @change="saveSettings()"
> >
<option value="left">Left</option> <option value="left">Left</option>
@ -34,11 +49,47 @@
@change="saveSettings()" @change="saveSettings()"
> >
<option value="player"> <option value="player">
When mouse hovers over player When mouse moves over player, always
</option> </option>
<option value="trigger-zone"> <option value="player-ctrl">
When mouse hovers over trigger zone When mouse moves over player, while holding CTRL key
</option> </option>
<option value="distance">
When mouse is close to the menu activator
</option>
<!-- <option value="trigger-zone">
When mouse moves over trigger zone
</option> -->
</select>
</div>
</div>
<div v-show="settings.active.ui.inPlayer.activation === 'distance'" class="field">
<div class="label">
Show menu when mouse is closer than:
</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.activationDistance"
class="slider"
type="range"
min="10"
max="100"
step="1"
@change="(event) => saveSettings()"
>
<input
style="margin-right: 0.6rem;"
v-model="settings.active.ui.inPlayer.activationDistance"
@change="(event) => saveSettings(true)"
>
<select
class="unit-select !min-w-[72px]"
v-model="settings.active.ui.inPlayer.activationDistanceUnit"
@change="(event) => saveSettings(true)"
>
<option value="%">%</option>
<option value="px">px</option>
</select> </select>
</div> </div>
</div> </div>