WIP: mini-window approach for scroll capture
Tentativa de capturar scroll via mini janela sempre-no-topo. Inclui: MoveWindow/GetMonitorWorkArea via Win32, park consistente em wx+150/wy+50, ShowCursor loop no goroutine pós-resize. Revertível: git checkout HEAD~1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6bdf5fcdbd
commit
5d016d7087
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(go build:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
48
app.go
48
app.go
@ -8,12 +8,19 @@ import (
|
||||
"kvmote/internal/input"
|
||||
"kvmote/internal/kvm"
|
||||
"kvmote/internal/transport"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
engine *kvm.Engine
|
||||
|
||||
// Estado da janela (pixels físicos Win32)
|
||||
origX, origY int32
|
||||
origW, origH int32
|
||||
isMini bool
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
@ -21,9 +28,48 @@ func NewApp() *App {
|
||||
t := transport.NewBleTransport()
|
||||
h := input.NewInputHandler()
|
||||
e := kvm.NewEngine(t, h)
|
||||
return &App{
|
||||
app := &App{
|
||||
engine: e,
|
||||
}
|
||||
e.SetWindowManager(app) // Vincula o app como gerenciador de janela da engine
|
||||
return app
|
||||
}
|
||||
|
||||
const miniW, miniH = int32(300), int32(100)
|
||||
|
||||
func (a *App) SetMiniMode() {
|
||||
if a.isMini {
|
||||
return
|
||||
}
|
||||
|
||||
// Salva posição/tamanho via Win32 (pixels físicos — mesma base do MoveWindow)
|
||||
wx, wy := a.engine.GetWindowPos()
|
||||
ww, wh := a.engine.GetWindowSize()
|
||||
if ww > 200 {
|
||||
a.origX, a.origY = wx, wy
|
||||
a.origW, a.origH = ww, wh
|
||||
}
|
||||
|
||||
a.isMini = true
|
||||
runtime.EventsEmit(a.ctx, "window-mode", "mini")
|
||||
|
||||
// Posiciona usando Win32 SetWindowPos (evita mistura de coordenadas lógicas/físicas)
|
||||
// MonitorFromWindow garante monitor correto em setups multi-monitor
|
||||
mx, my, mw, mh := a.engine.GetMonitorWorkArea()
|
||||
tx := mx + mw - miniW - 20
|
||||
ty := my + mh - miniH - 50
|
||||
a.engine.MoveWindow(tx, ty, miniW, miniH, true) // topmost=true
|
||||
}
|
||||
|
||||
func (a *App) RestoreNormalMode() {
|
||||
if !a.isMini {
|
||||
return
|
||||
}
|
||||
a.isMini = false
|
||||
runtime.EventsEmit(a.ctx, "window-mode", "normal")
|
||||
|
||||
// Restaura via Win32 (mesma base de coordenadas do SetMiniMode)
|
||||
a.engine.MoveWindow(a.origX, a.origY, a.origW, a.origH, false)
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
|
||||
166
frontend/dist/index.html
vendored
166
frontend/dist/index.html
vendored
@ -115,72 +115,97 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
.btn-cad { margin-top: 10px; background: #2a2a2a; color: #ccc; }
|
||||
|
||||
/* Estilo Neon para Modo Mini */
|
||||
.neon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background-color: black;
|
||||
cursor: default !important; /* Força cursor visível */
|
||||
}
|
||||
.neon-text {
|
||||
font-size: 1.6rem;
|
||||
font-weight: bold;
|
||||
color: #39FF14;
|
||||
text-shadow: 0 0 5px #39FF14, 0 0 10px #39FF14;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body x-data="kvmApp()">
|
||||
<div class="app-container">
|
||||
<div class="section-title">Posição do PC Cliente:</div>
|
||||
|
||||
<div class="monitor-grid">
|
||||
<!-- Top -->
|
||||
<button class="monitor-btn" style="grid-column: 2; grid-row: 1;"
|
||||
:class="pos === 'above' ? 'active' : ''" @click="setPos('above')">Acima</button>
|
||||
<body x-data="kvmApp()" style="cursor: default !important;">
|
||||
<template x-if="mode === 'normal'">
|
||||
<div class="app-container">
|
||||
<div class="section-title">Posição do PC Cliente:</div>
|
||||
|
||||
<!-- Left -->
|
||||
<button class="monitor-btn" style="grid-column: 1; grid-row: 2;"
|
||||
:class="pos === 'left' ? 'active' : ''" @click="setPos('left')">Esquerda</button>
|
||||
|
||||
<!-- Center -->
|
||||
<div class="host-pc">[HOST PC]</div>
|
||||
|
||||
<!-- Right -->
|
||||
<button class="monitor-btn" style="grid-column: 3; grid-row: 2;"
|
||||
:class="pos === 'right' ? 'active' : ''" @click="setPos('right')">Direita</button>
|
||||
|
||||
<!-- Bottom -->
|
||||
<button class="monitor-btn" style="grid-column: 2; grid-row: 3;"
|
||||
:class="pos === 'below' ? 'active' : ''" @click="setPos('below')">Abaixo</button>
|
||||
</div>
|
||||
|
||||
<div class="is-flex is-align-items-center mb-4" style="font-size: 0.85rem; color: #888;">
|
||||
<span class="status-dot" :style="{ backgroundColor: detected ? '#3498db' : '#555' }"></span>
|
||||
<span x-text="detected ? 'KVMote (BLE) detectado' : 'Buscando dispositivo...'"></span>
|
||||
</div>
|
||||
|
||||
<div class="columns is-mobile is-gapless mb-2">
|
||||
<div class="column pr-1">
|
||||
<button class="btn-action" @click="detect()">Detectar</button>
|
||||
<div class="monitor-grid">
|
||||
<!-- Top -->
|
||||
<button class="monitor-btn" style="grid-column: 2; grid-row: 1;"
|
||||
:class="pos === 'above' ? 'active' : ''" @click="setPos('above')">Acima</button>
|
||||
|
||||
<!-- Left -->
|
||||
<button class="monitor-btn" style="grid-column: 1; grid-row: 2;"
|
||||
:class="pos === 'left' ? 'active' : ''" @click="setPos('left')">Esquerda</button>
|
||||
|
||||
<!-- Center -->
|
||||
<div class="host-pc">[HOST PC]</div>
|
||||
|
||||
<!-- Right -->
|
||||
<button class="monitor-btn" style="grid-column: 3; grid-row: 2;"
|
||||
:class="pos === 'right' ? 'active' : ''" @click="setPos('right')">Direita</button>
|
||||
|
||||
<!-- Bottom -->
|
||||
<button class="monitor-btn" style="grid-column: 2; grid-row: 3;"
|
||||
:class="pos === 'below' ? 'active' : ''" @click="setPos('below')">Abaixo</button>
|
||||
</div>
|
||||
<div class="column pl-1">
|
||||
<button class="btn-action btn-connect" @click="toggleConnect()" x-text="connected ? 'Desconectar' : 'Conectar'"></button>
|
||||
|
||||
<div class="is-flex is-align-items-center mb-4" style="font-size: 0.85rem; color: #888;">
|
||||
<span class="status-dot" :style="{ backgroundColor: detected ? '#3498db' : '#555' }"></span>
|
||||
<span x-text="detected ? 'KVMote (BLE) detectado' : 'Buscando dispositivo...'"></span>
|
||||
</div>
|
||||
|
||||
<div class="columns is-mobile is-gapless mb-2">
|
||||
<div class="column pr-1">
|
||||
<button class="btn-action" @click="detect()">Detectar</button>
|
||||
</div>
|
||||
<div class="column pl-1">
|
||||
<button class="btn-action btn-connect" @click="toggleConnect()" x-text="connected ? 'Desconectar' : 'Conectar'"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field mb-4">
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between">
|
||||
<label class="field-label">Layout do cliente:</label>
|
||||
<select class="custom-select" style="width: 60%;" x-model="layout" @change="setLayout()">
|
||||
<option value="0">US / EN</option>
|
||||
<option value="1">PT-BR ABNT2</option>
|
||||
<option value="2">US International</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-action btn-cad" @click="sendCAD()">Enviar Ctrl+Alt+Del</button>
|
||||
|
||||
<div class="field mt-4">
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between">
|
||||
<label class="field-label">Ao fechar:</label>
|
||||
<select class="custom-select" style="width: 60%;">
|
||||
<option value="tray">Minimizar para Tray</option>
|
||||
<option value="exit">Fechar aplicação</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="field mb-4">
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between">
|
||||
<label class="field-label">Layout do cliente:</label>
|
||||
<select class="custom-select" style="width: 60%;" x-model="layout" @change="setLayout()">
|
||||
<option value="0">US / EN</option>
|
||||
<option value="1">PT-BR ABNT2</option>
|
||||
<option value="2">US International</option>
|
||||
</select>
|
||||
</div>
|
||||
<template x-if="mode === 'mini'">
|
||||
<div class="neon-container" id="mini-bg">
|
||||
<div class="neon-text" x-text="'KVMote [' + scrollCount + ']'"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button class="btn-action btn-cad" @click="sendCAD()">Enviar Ctrl+Alt+Del</button>
|
||||
|
||||
<div class="field mt-4">
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between">
|
||||
<label class="field-label">Ao fechar:</label>
|
||||
<select class="custom-select" style="width: 60%;">
|
||||
<option value="tray">Minimizar para Tray</option>
|
||||
<option value="exit">Fechar aplicação</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
<div class="status-bar" x-show="mode === 'normal'">
|
||||
<span x-text="statusText"></span>
|
||||
</div>
|
||||
|
||||
@ -197,6 +222,31 @@
|
||||
statusText: 'Desconectado',
|
||||
pos: 'right',
|
||||
layout: 1,
|
||||
mode: 'normal',
|
||||
scrollCount: 0,
|
||||
init() {
|
||||
if (window.runtime) {
|
||||
window.runtime.EventsOn("window-mode", (m) => {
|
||||
this.mode = m;
|
||||
});
|
||||
}
|
||||
|
||||
// Listener de Scroll interno ao kvmApp para acessar scrollCount
|
||||
window.addEventListener('wheel', (event) => {
|
||||
if (this.mode === 'mini') {
|
||||
this.scrollCount++;
|
||||
// Feedback visual rápido
|
||||
const bg = document.getElementById('mini-bg');
|
||||
if (bg) {
|
||||
bg.style.backgroundColor = '#1a3300';
|
||||
setTimeout(() => bg.style.backgroundColor = 'black', 50);
|
||||
}
|
||||
}
|
||||
if (window.go && window.go.main && window.go.main.App) {
|
||||
window.go.main.App.HandleScroll(Math.round(event.deltaY));
|
||||
}
|
||||
}, { passive: false });
|
||||
},
|
||||
toggleConnect() {
|
||||
if (this.connected) {
|
||||
window.go.main.App.Disconnect().then(res => {
|
||||
@ -229,9 +279,7 @@
|
||||
|
||||
// Captura o scroll globalmente na janela do KVMote
|
||||
window.addEventListener('wheel', (event) => {
|
||||
// Log no console do navegador para você ver se disparar (inspecionar elemento)
|
||||
console.log("Wheel event:", event.deltaY);
|
||||
|
||||
if (window.go && window.go.main && window.go.main.App) {
|
||||
window.go.main.App.HandleScroll(Math.round(event.deltaY));
|
||||
}
|
||||
|
||||
@ -9,6 +9,10 @@ export function Disconnect():Promise<string>;
|
||||
|
||||
export function HandleScroll(arg1:number):Promise<void>;
|
||||
|
||||
export function RestoreNormalMode():Promise<void>;
|
||||
|
||||
export function SendCtrlAltDel():Promise<void>;
|
||||
|
||||
export function SetMiniMode():Promise<void>;
|
||||
|
||||
export function SetPosition(arg1:number):Promise<void>;
|
||||
|
||||
@ -18,10 +18,18 @@ export function HandleScroll(arg1) {
|
||||
return window['go']['main']['App']['HandleScroll'](arg1);
|
||||
}
|
||||
|
||||
export function RestoreNormalMode() {
|
||||
return window['go']['main']['App']['RestoreNormalMode']();
|
||||
}
|
||||
|
||||
export function SendCtrlAltDel() {
|
||||
return window['go']['main']['App']['SendCtrlAltDel']();
|
||||
}
|
||||
|
||||
export function SetMiniMode() {
|
||||
return window['go']['main']['App']['SetMiniMode']();
|
||||
}
|
||||
|
||||
export function SetPosition(arg1) {
|
||||
return window['go']['main']['App']['SetPosition'](arg1);
|
||||
}
|
||||
|
||||
@ -27,4 +27,8 @@ type InputHandler interface {
|
||||
GetScreenResolution() (int32, int32)
|
||||
RequestFocus()
|
||||
SetCursorClip(clip bool)
|
||||
GetWindowPos() (int32, int32)
|
||||
GetWindowSize() (int32, int32)
|
||||
GetMonitorWorkArea() (x, y, w, h int32)
|
||||
MoveWindow(x, y, w, h int32, topmost bool)
|
||||
}
|
||||
|
||||
@ -29,12 +29,22 @@ var (
|
||||
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
|
||||
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||||
procClipCursor = user32.NewProc("ClipCursor")
|
||||
procSetWindowPos = user32.NewProc("SetWindowPos")
|
||||
procMonitorFromWindow = user32.NewProc("MonitorFromWindow")
|
||||
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
|
||||
)
|
||||
|
||||
type RECT struct {
|
||||
Left, Top, Right, Bottom int32
|
||||
}
|
||||
|
||||
type MONITORINFO struct {
|
||||
CbSize uint32
|
||||
RcMonitor RECT
|
||||
RcWork RECT
|
||||
DwFlags uint32
|
||||
}
|
||||
|
||||
const (
|
||||
WH_KEYBOARD_LL = 13
|
||||
WH_MOUSE_LL = 14
|
||||
@ -195,3 +205,54 @@ func (h *windowsInputHandler) SetCursorClip(clip bool) {
|
||||
procClipCursor.Call(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *windowsInputHandler) GetWindowPos() (int32, int32) {
|
||||
if h.hwnd != 0 {
|
||||
var rect RECT
|
||||
procGetWindowRect.Call(h.hwnd, uintptr(unsafe.Pointer(&rect)))
|
||||
return rect.Left, rect.Top
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (h *windowsInputHandler) GetWindowSize() (int32, int32) {
|
||||
if h.hwnd != 0 {
|
||||
var rect RECT
|
||||
procGetWindowRect.Call(h.hwnd, uintptr(unsafe.Pointer(&rect)))
|
||||
return rect.Right - rect.Left, rect.Bottom - rect.Top
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (h *windowsInputHandler) GetMonitorWorkArea() (x, y, w, ht int32) {
|
||||
if h.hwnd == 0 {
|
||||
return 0, 0, 1920, 1080
|
||||
}
|
||||
monitor, _, _ := procMonitorFromWindow.Call(h.hwnd, 2) // MONITOR_DEFAULTTONEAREST
|
||||
if monitor == 0 {
|
||||
return 0, 0, 1920, 1080
|
||||
}
|
||||
var mi MONITORINFO
|
||||
mi.CbSize = uint32(unsafe.Sizeof(mi))
|
||||
procGetMonitorInfoW.Call(monitor, uintptr(unsafe.Pointer(&mi)))
|
||||
return mi.RcWork.Left, mi.RcWork.Top,
|
||||
mi.RcWork.Right - mi.RcWork.Left,
|
||||
mi.RcWork.Bottom - mi.RcWork.Top
|
||||
}
|
||||
|
||||
func (h *windowsInputHandler) MoveWindow(x, y, w, ht int32, topmost bool) {
|
||||
if h.hwnd == 0 {
|
||||
return
|
||||
}
|
||||
// HWND_TOPMOST = -1, HWND_NOTOPMOST = -2
|
||||
zorder := ^uintptr(1) // HWND_NOTOPMOST
|
||||
if topmost {
|
||||
zorder = ^uintptr(0) // HWND_TOPMOST
|
||||
}
|
||||
const SWP_NOACTIVATE = 0x0010
|
||||
procSetWindowPos.Call(
|
||||
h.hwnd, zorder,
|
||||
uintptr(x), uintptr(y), uintptr(w), uintptr(ht),
|
||||
SWP_NOACTIVATE,
|
||||
)
|
||||
}
|
||||
|
||||
@ -40,10 +40,16 @@ const (
|
||||
LayoutUsIntl
|
||||
)
|
||||
|
||||
type WindowManager interface {
|
||||
SetMiniMode()
|
||||
RestoreNormalMode()
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
mu sync.Mutex
|
||||
transport transport.Transport
|
||||
inputHandler input.InputHandler
|
||||
winManager WindowManager
|
||||
|
||||
clientMode bool
|
||||
clientPos ClientPos
|
||||
@ -76,35 +82,40 @@ func NewEngine(t transport.Transport, h input.InputHandler) *Engine {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) SetWindowManager(wm WindowManager) {
|
||||
e.winManager = wm
|
||||
}
|
||||
|
||||
func (e *Engine) Transport() transport.Transport {
|
||||
return e.transport
|
||||
}
|
||||
|
||||
func (e *Engine) GetScreenResolution() (int32, int32) {
|
||||
return e.inputHandler.GetScreenResolution()
|
||||
}
|
||||
|
||||
func (e *Engine) GetWindowSize() (int32, int32) {
|
||||
return e.inputHandler.GetWindowSize()
|
||||
}
|
||||
|
||||
func (e *Engine) GetWindowPos() (int32, int32) {
|
||||
return e.inputHandler.GetWindowPos()
|
||||
}
|
||||
|
||||
func (e *Engine) GetMonitorWorkArea() (x, y, w, h int32) {
|
||||
return e.inputHandler.GetMonitorWorkArea()
|
||||
}
|
||||
|
||||
func (e *Engine) MoveWindow(x, y, w, h int32, topmost bool) {
|
||||
e.inputHandler.MoveWindow(x, y, w, h, topmost)
|
||||
}
|
||||
|
||||
func (e *Engine) Start(ctx context.Context) error {
|
||||
w, h := e.inputHandler.GetScreenResolution()
|
||||
LogDebug(fmt.Sprintf("Engine Iniciada. Tela: %dx%d. Pos: %v", w, h, e.clientPos))
|
||||
return e.inputHandler.Install(ctx, e.onMouse, e.onKey)
|
||||
}
|
||||
|
||||
func (e *Engine) processarScroll(msg uint32, data uint32) {
|
||||
e.scrollActive = true
|
||||
e.scrollTimer = time.Now()
|
||||
|
||||
deltaRaw := int16(data >> 16)
|
||||
|
||||
// LOG TOTAL PARA DESCOBRIR O QUE O TOUCHPAD MANDA
|
||||
go LogDebug(fmt.Sprintf("SCROLL RAW: msg=0x%X data=0x%X delta=%d", msg, data, deltaRaw))
|
||||
|
||||
if deltaRaw > 0 {
|
||||
e.transport.Send([]byte{'P', 0xDA}) // Up
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
e.transport.Send([]byte{'U', 0xDA})
|
||||
} else if deltaRaw < 0 {
|
||||
e.transport.Send([]byte{'P', 0xD9}) // Down
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
e.transport.Send([]byte{'U', 0xD9})
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
||||
e.mu.Lock()
|
||||
@ -131,50 +142,48 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
||||
|
||||
switch ev.Message {
|
||||
case 0x020A, 0x020E: // Roda Vertical ou Horizontal
|
||||
e.processarScroll(ev.Message, ev.Data)
|
||||
return true
|
||||
// Não bloquear: evento passa para o webview → JS wheel → HandleManualScroll
|
||||
// (WH_MOUSE_LL não recebe scroll de touchpad precision; mini janela captura via JS)
|
||||
e.scrollActive = true
|
||||
e.scrollTimer = time.Now()
|
||||
return false
|
||||
|
||||
case 0x0200: // Move
|
||||
if e.isWarping { e.isWarping = false; return true }
|
||||
|
||||
wx, wy := e.inputHandler.GetWindowPos()
|
||||
|
||||
if e.scrollActive {
|
||||
// Se estiver scrollando, ignoramos movimentos por um tempo curto (touchpads)
|
||||
// Durante scroll: decai após 250ms, mas sempre parka cursor na janela
|
||||
if time.Since(e.scrollTimer) > 250*time.Millisecond {
|
||||
e.scrollActive = false
|
||||
e.virtualX, e.virtualY = 0, 0
|
||||
}
|
||||
e.lastRawPos = ev.Point
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
dx, dy := ev.Point.X-e.lastRawPos.X, ev.Point.Y-e.lastRawPos.Y
|
||||
e.virtualX += dx
|
||||
e.virtualY += dy
|
||||
e.pendingDX += dx
|
||||
e.pendingDY += dy
|
||||
|
||||
dx, dy := ev.Point.X - e.lastRawPos.X, ev.Point.Y - e.lastRawPos.Y
|
||||
e.virtualX += dx
|
||||
e.virtualY += dy
|
||||
e.pendingDX += dx
|
||||
e.pendingDY += dy
|
||||
|
||||
if e.shouldReturnToHost() {
|
||||
if time.Since(e.lastModeChange) > 800*time.Millisecond {
|
||||
e.exitClientMode()
|
||||
return true
|
||||
if e.shouldReturnToHost() {
|
||||
if time.Since(e.lastModeChange) > 800*time.Millisecond {
|
||||
e.exitClientMode()
|
||||
return true
|
||||
}
|
||||
e.virtualX, e.virtualY = 0, 0
|
||||
} else if time.Since(e.mouseThrottle) >= 40*time.Millisecond {
|
||||
e.mouseThrottle = time.Now()
|
||||
sdx, sdy := int8(clamp(int(e.pendingDX), -127, 127)), int8(clamp(int(e.pendingDY), -127, 127))
|
||||
e.pendingDX, e.pendingDY = 0, 0
|
||||
e.transport.SendLossy([]byte{'M', byte(sdx), byte(sdy)})
|
||||
}
|
||||
e.virtualX, e.virtualY = 0, 0
|
||||
return true
|
||||
}
|
||||
|
||||
// Park no centro para manter o mouse sobre a janela do App
|
||||
// permitindo a captura do scroll.
|
||||
w, h := e.inputHandler.GetScreenResolution()
|
||||
// Sempre parka cursor no centro da mini janela (garante alvo para scroll)
|
||||
e.isWarping = true
|
||||
e.inputHandler.SetCursorPos(w/2, h/2)
|
||||
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
|
||||
|
||||
if time.Since(e.mouseThrottle) >= 40*time.Millisecond {
|
||||
e.mouseThrottle = time.Now()
|
||||
sdx, sdy := int8(clamp(int(e.pendingDX), -127, 127)), int8(clamp(int(e.pendingDY), -127, 127))
|
||||
e.pendingDX, e.pendingDY = 0, 0
|
||||
e.transport.SendLossy([]byte{'M', byte(sdx), byte(sdy)})
|
||||
}
|
||||
e.inputHandler.SetCursorPos(wx+150, wy+50)
|
||||
e.lastRawPos = input.Point{X: wx + 150, Y: wy + 50}
|
||||
return true
|
||||
|
||||
case 0x0201: e.transport.Send([]byte{'D', 'L'}); return true
|
||||
@ -218,17 +227,31 @@ func (e *Engine) enterClientMode(p input.Point) {
|
||||
e.wheelAccum = 0
|
||||
e.mouseThrottle = time.Now()
|
||||
|
||||
// Foco para receber scroll da interface
|
||||
e.inputHandler.RequestFocus()
|
||||
|
||||
w, h := e.inputHandler.GetScreenResolution()
|
||||
e.isWarping = true
|
||||
// Park no centro como no início
|
||||
e.inputHandler.SetCursorPos(w/2, h/2)
|
||||
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
|
||||
|
||||
// Sem esconder cursor para teste de scroll puro
|
||||
e.inputHandler.ShowCursor(true)
|
||||
if e.winManager != nil {
|
||||
e.winManager.SetMiniMode()
|
||||
}
|
||||
|
||||
// Wails move janela de forma assíncrona; aguardar antes de parkear
|
||||
// ShowCursor(true) DEVE ser após resize/move: Wails/webview pode chamar ShowCursor(false) internamente
|
||||
go func() {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if !e.clientMode {
|
||||
return
|
||||
}
|
||||
// Força cursor visível (combate ShowCursor(false) interno do Wails/webview)
|
||||
for i := 0; i < 10; i++ {
|
||||
e.inputHandler.ShowCursor(true)
|
||||
}
|
||||
wx, wy := e.inputHandler.GetWindowPos()
|
||||
e.isWarping = true
|
||||
e.inputHandler.SetCursorPos(wx+150, wy+50)
|
||||
e.lastRawPos = input.Point{X: wx + 150, Y: wy + 50}
|
||||
LogDebug(fmt.Sprintf("Cursor parkado em mini: (%d,%d)", wx+150, wy+50))
|
||||
}()
|
||||
|
||||
// 'A' (ReleaseAll) limpa estados presos no firmware, 'O' sinaliza LED Magenta
|
||||
e.transport.Send([]byte{'A'})
|
||||
@ -239,6 +262,12 @@ func (e *Engine) exitClientMode() {
|
||||
LogDebug("Saindo Modo Cliente.")
|
||||
e.clientMode = false
|
||||
e.lastModeChange = time.Now()
|
||||
|
||||
// Restaura a janela (Normal Mode)
|
||||
if e.winManager != nil {
|
||||
e.winManager.RestoreNormalMode()
|
||||
}
|
||||
|
||||
e.inputHandler.ShowCursor(true)
|
||||
w, h := e.inputHandler.GetScreenResolution()
|
||||
var ret input.Point
|
||||
|
||||
11
main.go
11
main.go
@ -6,6 +6,7 @@ import (
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
@ -20,6 +21,11 @@ func main() {
|
||||
Title: "KVMote",
|
||||
Width: 400,
|
||||
Height: 550,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: false,
|
||||
MinWidth: 200,
|
||||
MinHeight: 50,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
@ -28,6 +34,11 @@ func main() {
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
Windows: &windows.Options{
|
||||
DisableWindowIcon: false,
|
||||
WebviewIsTransparent: false,
|
||||
WindowIsTranslucent: false,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user