Add hover sync and CO hover component
Add a hover system to restore :hover-like behavior for custom UI elements in the CommonwealthOnline view. Changes include: - plugin/src/F4TPrismaUI.cpp: add SyncBrowserHover() which reads the native cursor position, converts it to client coordinates, and invokes a JS update on the server browser view; register a periodic task to call SyncBrowserHover every other tick. - ui/views/CommonwealthOnline/browser/components/hover.js: new component that tracks pointer position, determines hovered targets using a selector list, and applies/removes a .hovered class; exposes bind(), updateFromPoint(), and clear(). - ui/views/CommonwealthOnline/browser/app.js & index.html: include and bind the new hover component during UI initialization. - ui/views/CommonwealthOnline/browser/styles.css: add .hovered variants and transitions for nav items, buttons, rows, and filter controls to match hover visuals. - steam_interfaces.txt: add a list of Steam interface version strings (new file). Purpose: ensure custom buttons/rows that don't receive native :hover updates from the renderer get visible hover feedback by syncing the native cursor into the web UI and toggling a .hovered class.
This commit is contained in:
@@ -76,6 +76,31 @@ namespace F4T::PrismaUI
|
|||||||
g_cursorRestoreFramesLeft = 4;
|
g_cursorRestoreFramesLeft = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SyncBrowserHover()
|
||||||
|
{
|
||||||
|
if (!g_visible || !g_domReady || !g_api || !g_api->IsValid(g_serverBrowserView)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* renderWindow = RE::BSGraphics::GetCurrentRendererWindow();
|
||||||
|
if (!renderWindow || !renderWindow->hwnd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT cursor{};
|
||||||
|
if (!::GetCursorPos(&cursor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ScreenToClient(reinterpret_cast<HWND>(renderWindow->hwnd), &cursor);
|
||||||
|
|
||||||
|
const auto script = std::format(
|
||||||
|
"if(window.CO_Hover&&window.CO_Hover.updateFromPoint){{window.CO_Hover.updateFromPoint({},{});}}",
|
||||||
|
cursor.x,
|
||||||
|
cursor.y);
|
||||||
|
g_api->Invoke(g_serverBrowserView, script.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
void HideServerBrowserInternal()
|
void HideServerBrowserInternal()
|
||||||
{
|
{
|
||||||
const bool wasVisible = g_visible;
|
const bool wasVisible = g_visible;
|
||||||
@@ -283,6 +308,14 @@ namespace F4T::PrismaUI
|
|||||||
taskInterface->AddTaskPermanent(WatchMainMenuState);
|
taskInterface->AddTaskPermanent(WatchMainMenuState);
|
||||||
g_menuWatchTaskStarted = true;
|
g_menuWatchTaskStarted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
taskInterface->AddTaskPermanent([]() {
|
||||||
|
static std::uint32_t hoverSyncCounter = 0;
|
||||||
|
if (++hoverSyncCounter % 2 != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SyncBrowserHover();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
SteamClient017
|
||||||
|
SteamClient020
|
||||||
|
SteamGameServer014
|
||||||
|
SteamGameServerStats001
|
||||||
|
SteamUser021
|
||||||
|
SteamFriends017
|
||||||
|
SteamUtils010
|
||||||
|
SteamMatchMaking009
|
||||||
|
SteamMatchMakingServers002
|
||||||
|
STEAMUSERSTATS_INTERFACE_VERSION012
|
||||||
|
STEAMAPPS_INTERFACE_VERSION008
|
||||||
|
SteamNetworking006
|
||||||
|
STEAMREMOTESTORAGE_INTERFACE_VERSION016
|
||||||
|
STEAMSCREENSHOTS_INTERFACE_VERSION003
|
||||||
|
STEAMHTTP_INTERFACE_VERSION003
|
||||||
|
STEAMUGC_INTERFACE_VERSION016
|
||||||
|
STEAMAPPLIST_INTERFACE_VERSION001
|
||||||
|
STEAMMUSIC_INTERFACE_VERSION001
|
||||||
|
STEAMMUSICREMOTE_INTERFACE_VERSION001
|
||||||
|
STEAMHTMLSURFACE_INTERFACE_VERSION_005
|
||||||
|
STEAMINVENTORY_INTERFACE_V003
|
||||||
|
SteamController008
|
||||||
|
STEAMVIDEO_INTERFACE_V002
|
||||||
@@ -296,6 +296,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindUi() {
|
function bindUi() {
|
||||||
|
if (window.CO_Hover) {
|
||||||
|
CO_Hover.bind(root);
|
||||||
|
}
|
||||||
|
|
||||||
CO_Tabs.bind(root, function (tabName) {
|
CO_Tabs.bind(root, function (tabName) {
|
||||||
switchTab(tabName);
|
switchTab(tabName);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PrismaUI / Ultralight updates :hover only for native form controls (input, select).
|
||||||
|
* Custom buttons and rows need a .hovered class driven by cursor position.
|
||||||
|
*/
|
||||||
|
var HOVER_SELECTOR = [
|
||||||
|
".co-nav-item",
|
||||||
|
".co-server-row",
|
||||||
|
".co-btn",
|
||||||
|
".co-checkbox-row"
|
||||||
|
].join(", ");
|
||||||
|
|
||||||
|
var currentHovered = null;
|
||||||
|
var lastX = null;
|
||||||
|
var lastY = null;
|
||||||
|
var rafId = null;
|
||||||
|
var boundPanel = null;
|
||||||
|
|
||||||
|
function findHoverTarget(node) {
|
||||||
|
if (!node || !node.closest) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = node.closest(HOVER_SELECTOR);
|
||||||
|
if (!target || !boundPanel || !boundPanel.contains(target)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHovered(target) {
|
||||||
|
if (target === currentHovered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentHovered) {
|
||||||
|
currentHovered.classList.remove("hovered");
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHovered = target;
|
||||||
|
|
||||||
|
if (currentHovered) {
|
||||||
|
currentHovered.classList.add("hovered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHovered() {
|
||||||
|
setHovered(null);
|
||||||
|
lastX = null;
|
||||||
|
lastY = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInsidePanel(x, y) {
|
||||||
|
if (!boundPanel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rect = boundPanel.getBoundingClientRect();
|
||||||
|
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromPoint(x, y) {
|
||||||
|
if (typeof x !== "number" || typeof y !== "number" || isNaN(x) || isNaN(y)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastX = x;
|
||||||
|
lastY = y;
|
||||||
|
|
||||||
|
if (currentHovered && !document.body.contains(currentHovered)) {
|
||||||
|
currentHovered = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInsidePanel(x, y)) {
|
||||||
|
setHovered(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var node = document.elementFromPoint(x, y);
|
||||||
|
setHovered(findHoverTarget(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerEvent(e) {
|
||||||
|
updateFromPoint(e.clientX, e.clientY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
if (lastX !== null && lastY !== null) {
|
||||||
|
updateFromPoint(lastX, lastY);
|
||||||
|
}
|
||||||
|
rafId = window.requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CO_Hover = {
|
||||||
|
bind: function (root) {
|
||||||
|
boundPanel = root && root.querySelector(".co-panel");
|
||||||
|
if (!boundPanel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", onPointerEvent, true);
|
||||||
|
document.addEventListener("pointermove", onPointerEvent, true);
|
||||||
|
|
||||||
|
boundPanel.addEventListener("mouseleave", clearHovered);
|
||||||
|
|
||||||
|
if (!rafId) {
|
||||||
|
rafId = window.requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFromPoint: updateFromPoint,
|
||||||
|
|
||||||
|
clear: clearHovered
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -87,6 +87,7 @@
|
|||||||
<script src="./components/server-details.js"></script>
|
<script src="./components/server-details.js"></script>
|
||||||
<script src="./components/controller-prompts.js"></script>
|
<script src="./components/controller-prompts.js"></script>
|
||||||
<script src="./components/direct-connect.js"></script>
|
<script src="./components/direct-connect.js"></script>
|
||||||
|
<script src="./components/hover.js"></script>
|
||||||
<script src="./app.js"></script>
|
<script src="./app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -172,10 +172,15 @@ body {
|
|||||||
transition: background var(--co-transition), color var(--co-transition);
|
transition: background var(--co-transition), color var(--co-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-nav-item:hover:not(.active) {
|
.co-nav-item:hover:not(.active),
|
||||||
|
.co-nav-item.hovered:not(.active) {
|
||||||
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.08);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.co-nav-item.active.hovered {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
.co-nav-item.active {
|
.co-nav-item.active {
|
||||||
background: var(--co-selected);
|
background: var(--co-selected);
|
||||||
color: var(--co-selected-text);
|
color: var(--co-selected-text);
|
||||||
@@ -369,12 +374,20 @@ body {
|
|||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
cursor: var(--co-cursor);
|
cursor: var(--co-cursor);
|
||||||
|
transition: border-color var(--co-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-filter-input::placeholder {
|
.co-filter-input::placeholder {
|
||||||
color: var(--co-text-dim);
|
color: var(--co-text-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.co-filter-input:hover,
|
||||||
|
.co-filter-input.hovered,
|
||||||
|
.co-filter-select:hover,
|
||||||
|
.co-filter-select.hovered {
|
||||||
|
border-color: rgba(var(--co-r), var(--co-g), var(--co-b), 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
.co-filter-input:focus,
|
.co-filter-input:focus,
|
||||||
.co-filter-select:focus {
|
.co-filter-select:focus {
|
||||||
border-color: var(--co-border);
|
border-color: var(--co-border);
|
||||||
@@ -390,6 +403,12 @@ body {
|
|||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: var(--co-cursor);
|
cursor: var(--co-cursor);
|
||||||
|
transition: background var(--co-transition), border-color var(--co-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-checkbox-row:hover,
|
||||||
|
.co-checkbox-row.hovered {
|
||||||
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-checkbox {
|
.co-checkbox {
|
||||||
@@ -425,11 +444,13 @@ body {
|
|||||||
cursor: var(--co-cursor);
|
cursor: var(--co-cursor);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: background var(--co-transition);
|
transition: background var(--co-transition), border-color var(--co-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-btn:hover {
|
.co-btn:hover,
|
||||||
|
.co-btn.hovered {
|
||||||
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.1);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.1);
|
||||||
|
border-color: rgba(var(--co-r), var(--co-g), var(--co-b), 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-btn.focused {
|
.co-btn.focused {
|
||||||
@@ -475,10 +496,11 @@ body {
|
|||||||
font-size: var(--co-font-md);
|
font-size: var(--co-font-md);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
cursor: var(--co-cursor);
|
cursor: var(--co-cursor);
|
||||||
transition: background var(--co-transition);
|
transition: background var(--co-transition), color var(--co-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.co-server-row:hover:not(.selected) {
|
.co-server-row:hover:not(.selected),
|
||||||
|
.co-server-row.hovered:not(.selected) {
|
||||||
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.06);
|
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user