Files
cthulhu/internal/app/wine_review_focus.go

126 lines
4.9 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
"git.stormux.org/storm/cthulhu/internal/review"
"git.stormux.org/storm/cthulhu/internal/wine"
"log/slog"
)
type wineReviewFocusRegistry interface {
Focuses() []wine.ReviewFocusNotice
Current(atspi.ObjectID) bool
}
// Only the event loop calls this controller. Inputs are copied metadata;
// foreground queries read the adapter's atomic cache and perform no native I/O.
type wineReviewFocusController struct {
registry wineReviewFocusRegistry
logger *slog.Logger
state *eventLoopState
foreground foregroundMonitor
titles windowTitleSource
gate *focusGate
worker *review.Worker
clear func()
active wine.ReviewFocusNotice
epoch uint64
}
func wineReviewBoundsMatch(bounds atspi.Rectangle, client xorg.Foreground) bool {
return client.Known && client.Window != 0 && bounds.Valid() && ((int64(bounds.X) == int64(client.X) && int64(bounds.Y) == int64(client.Y) && int64(bounds.Width) == int64(client.Width) && int64(bounds.Height) == int64(client.Height)) ||
(int64(bounds.X) == int64(client.OuterX) && int64(bounds.Y) == int64(client.OuterY) && int64(bounds.Width) == int64(client.OuterWidth) && int64(bounds.Height) == int64(client.OuterHeight)))
}
func (c *wineReviewFocusController) eligible(notice wine.ReviewFocusNotice, fg atspi.ForegroundState, title atspi.ForegroundTitle) bool {
metadata := title.Title
if !fg.Client.Known || fg.Client.Window == 0 || title.Epoch != fg.Epoch || metadata.Window != fg.Client.Window || !metadata.Wine || !metadata.Focused || !equivalentWindowTitles(metadata.Title, notice.Title) {
return false
}
if metadata.TitleWindow == metadata.Window {
return wineReviewBoundsMatch(notice.Bounds, fg.Client)
}
if metadata.TitleWindow == 0 || metadata.Client.Window != metadata.TitleWindow {
return false
}
if wineReviewBoundsMatch(notice.Bounds, metadata.Client) {
return true
}
// Wine's virtual desktop uses desktop-local screen coordinates. Corroborate
// a translated rectangle against the actual focused X11 child, not a title.
return int64(notice.Bounds.X)+int64(fg.Client.X) == int64(metadata.Client.X) && int64(notice.Bounds.Y)+int64(fg.Client.Y) == int64(metadata.Client.Y) &&
int64(notice.Bounds.Width) == int64(metadata.Client.Width) && int64(notice.Bounds.Height) == int64(metadata.Client.Height) && notice.Bounds.Valid()
}
func (c *wineReviewFocusController) changed() {
if c == nil || c.registry == nil || c.worker == nil || c.foreground == nil || c.titles == nil || c.gate.Locked() {
if c != nil {
c.retire()
}
return
}
fg, title := c.foreground.Foreground(), c.titles.ForegroundTitle()
var selected wine.ReviewFocusNotice
count := 0
for _, notice := range c.registry.Focuses() {
eligible := c.eligible(notice, fg, title)
if !eligible && c.logger != nil {
c.logger.Debug("Wine review foreground mismatch", "sender", notice.Sender,
"window", notice.Window, "focus", notice.Focus, "revision", notice.Revision,
"source_bounds", notice.Bounds, "native_foreground", fg.Client,
"foreground_epoch", fg.Epoch, "title_epoch", title.Epoch, "title_metadata", title.Title)
}
if eligible {
selected = notice
count++
}
}
if count != 1 || !c.registry.Current(selected.ID()) {
c.retire()
return
}
c.gate.SetSuspended(false)
transition := c.active.ID() != selected.ID() || c.active.Revision != selected.Revision || c.active.Protected != selected.Protected || c.epoch != fg.Epoch
if transition {
if c.clear != nil {
c.clear()
} else {
c.worker.ClearFocus()
}
c.worker.SyncFocus(selected.ID())
}
c.active, c.epoch = selected, fg.Epoch
c.state.current = atspi.Snapshot{BusName: selected.Sender, Path: selected.ID().Path, Role: "text"}
if selected.Protected {
c.state.current.Role = "password text"
}
c.state.focusedID = selected.ID()
c.state.owner = windowOwner{applicationBus: selected.Sender, window: wine.ReviewID(selected.Sender, selected.Instance, selected.Window, selected.Window), focus: selected.ID(), activated: true, title: selected.Title}
}
func (c *wineReviewFocusController) foregroundChanged() { c.changed() }
func (c *wineReviewFocusController) retire() {
if c == nil || c.active.Sender == "" {
return
}
if c.worker != nil && c.worker.Focus() == c.active.ID() {
if c.clear != nil {
c.clear()
} else {
c.worker.ClearFocus()
}
}
if c.state != nil && c.state.owner.applicationBus == c.active.Sender {
c.state.owner.clear()
}
c.active = wine.ReviewFocusNotice{}
c.epoch = 0
}
func (c *wineReviewFocusController) owns(id atspi.ObjectID) bool {
return c != nil && c.active.Sender != "" && c.active.ID() == id && c.registry.Current(id)
}
func (c *wineReviewFocusController) resultCurrent(result review.Result) bool {
c.changed()
return c.owns(result.FlatFocus) && flatReviewResultCurrent(c.worker, result)
}