Files
deskview/internal/ui/overlay.go
T
2026-10-28 08:47:09 +08:00

301 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ui
import (
"fmt"
"image"
"image/color"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"viewsagent/internal/capture"
"viewsagent/internal/win32"
)
// overlayTitle 是框选窗口标题,win32 需要靠它找到 HWND。
const overlayTitle = "viewsagent-overlay"
// overlay 是一个铺满当前显示器的冻结帧框选层。
//
// 之所以不依赖窗口透明:Windows 下的 OpenGL 窗口透明并不可靠,
// 直接用"全屏截图做背景 + 遮罩"视觉等效,还能保证选区与截图完全对齐。
type overlay struct {
win fyne.Window
vd win32.Rect
mapping capture.Mapping
size fyne.Size
calibrated bool
bg *canvas.Image
dims [4]*canvas.Rectangle
edges [4]*canvas.Rectangle
hint *canvas.Text
catcher *catcher
start, cur fyne.Position
dragging bool
closed bool
onDone func(capture.Region)
onCancel func()
}
// newOverlay 创建框选层。shot 应该是调用前抓好的整屏画面。
func newOverlay(app fyne.App, shot image.Image, vd win32.Rect,
onDone func(capture.Region), onCancel func()) *overlay {
o := &overlay{vd: vd, onDone: onDone, onCancel: onCancel}
o.win = app.NewWindow(overlayTitle)
o.win.SetPadded(false)
o.win.SetFullScreen(true)
o.bg = canvas.NewImageFromImage(shot)
o.bg.FillMode = canvas.ImageFillStretch
objs := []fyne.CanvasObject{o.bg}
for i := range o.dims {
o.dims[i] = canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 130})
objs = append(objs, o.dims[i])
}
for i := range o.edges {
// 描边跟着主题走:黑白主题下是纯白/纯黑,不会残留彩色。
o.edges[i] = canvas.NewRectangle(lookupColor(theme.ColorNameForeground))
objs = append(objs, o.edges[i])
}
o.hint = canvas.NewText("拖拽框选区域 · Esc 取消", color.White)
o.hint.TextStyle = fyne.TextStyle{Bold: true}
o.hint.TextSize = 15
o.catcher = newCatcher(o)
objs = append(objs, o.catcher, o.hint)
o.win.SetContent(container.NewWithoutLayout(objs...))
o.win.Canvas().SetOnTypedKey(func(k *fyne.KeyEvent) {
if k.Name == fyne.KeyEscape {
o.cancel()
}
})
return o
}
// show 显示框选层并做一次坐标校准。
func (o *overlay) show() {
o.win.Show()
// 窗口尺寸在 Show 之后才确定,稍等一拍再校准。
go func() {
time.Sleep(90 * time.Millisecond)
fyne.Do(o.calibrate)
}()
}
// calibrate 反推"逻辑坐标 → 物理像素"的比例与原点。
// 宁可依赖实际窗口尺寸,也不假设系统缩放,这样 125%/150% 屏都不会偏。
func (o *overlay) calibrate() {
size := o.win.Canvas().Size()
if size.Width < 10 || size.Height < 10 {
return
}
o.size = size
rect, err := windowRectByTitle(overlayTitle)
if err != nil {
mon, _ := win32.MonitorAtCursor()
rect = mon.Rect
}
if rect.Width() <= 0 || rect.Height() <= 0 {
return
}
o.mapping = capture.Mapping{
OffsetX: float64(rect.Left),
OffsetY: float64(rect.Top),
ScaleX: float64(rect.Width()) / float64(size.Width),
ScaleY: float64(rect.Height()) / float64(size.Height),
}
if o.mapping.ScaleX <= 0 || o.mapping.ScaleY <= 0 {
o.mapping.ScaleX, o.mapping.ScaleY = 1, 1
}
// 把整张虚拟桌面按当前窗口的实际映射铺到画布上。
bx, by, bw, bh := o.mapping.ToLogical(capture.Region{
X: o.vd.Left, Y: o.vd.Top, W: o.vd.Width(), H: o.vd.Height(),
})
o.bg.Move(fyne.NewPos(float32(bx), float32(by)))
o.bg.Resize(fyne.NewSize(float32(bw), float32(bh)))
o.catcher.Move(fyne.NewPos(0, 0))
o.catcher.Resize(size)
o.calibrated = true
o.updateSelection()
}
func windowRectByTitle(title string) (win32.Rect, error) {
hwnd, err := win32.FindWindowByTitle(title)
if err != nil {
return win32.Rect{}, err
}
return win32.WindowRect(hwnd)
}
// selection 返回当前选区(逻辑坐标,已归一化)。
func (o *overlay) selection() (x0, y0, x1, y1 float32) {
x0, y0 = o.start.X, o.start.Y
x1, y1 = o.cur.X, o.cur.Y
if x1 < x0 {
x0, x1 = x1, x0
}
if y1 < y0 {
y0, y1 = y1, y0
}
// 限制在窗口内
x0 = fyne.Max(x0, 0)
y0 = fyne.Max(y0, 0)
x1 = fyne.Min(x1, o.size.Width)
y1 = fyne.Min(y1, o.size.Height)
return
}
// updateSelection 重排遮罩、边框与尺寸提示。
func (o *overlay) updateSelection() {
if o.size.Width <= 0 {
return
}
w, h := o.size.Width, o.size.Height
x0, y0, x1, y1 := o.selection()
selW, selH := x1-x0, y1-y0
if selW < 2 || selH < 2 {
// 尚未框选:整屏压暗
o.dims[0].Move(fyne.NewPos(0, 0))
o.dims[0].Resize(fyne.NewSize(w, h))
for i := 1; i < 4; i++ {
o.dims[i].Resize(fyne.NewSize(0, 0))
}
for _, e := range o.edges {
e.Resize(fyne.NewSize(0, 0))
}
o.hint.Text = "拖拽框选区域 · Esc 取消"
o.hint.Move(fyne.NewPos(theme.Padding()*2, theme.Padding()*2))
o.hint.Refresh()
return
}
// 上下左右四块压暗区域,挖出选区
o.dims[0].Move(fyne.NewPos(0, 0))
o.dims[0].Resize(fyne.NewSize(w, y0))
o.dims[1].Move(fyne.NewPos(0, y1))
o.dims[1].Resize(fyne.NewSize(w, h-y1))
o.dims[2].Move(fyne.NewPos(0, y0))
o.dims[2].Resize(fyne.NewSize(x0, selH))
o.dims[3].Move(fyne.NewPos(x1, y0))
o.dims[3].Resize(fyne.NewSize(w-x1, selH))
// 选区描边
const t = 2
o.edges[0].Move(fyne.NewPos(x0, y0))
o.edges[0].Resize(fyne.NewSize(selW, t))
o.edges[1].Move(fyne.NewPos(x0, y1-t))
o.edges[1].Resize(fyne.NewSize(selW, t))
o.edges[2].Move(fyne.NewPos(x0, y0))
o.edges[2].Resize(fyne.NewSize(t, selH))
o.edges[3].Move(fyne.NewPos(x1-t, y0))
o.edges[3].Resize(fyne.NewSize(t, selH))
phys := o.mapping.ToPhysical(float64(x0), float64(y0), float64(selW), float64(selH))
o.hint.Text = fmt.Sprintf("%d × %d", phys.W, phys.H)
hy := y0 - 26
if hy < 4 {
hy = y0 + 6
}
o.hint.Move(fyne.NewPos(x0+6, hy))
o.hint.Refresh()
}
// finish 确认选区。
func (o *overlay) finish() {
if !o.calibrated {
o.calibrate()
}
x0, y0, x1, y1 := o.selection()
if x1-x0 < 6 || y1-y0 < 6 {
o.cancel()
return
}
r := o.mapping.ToPhysical(float64(x0), float64(y0), float64(x1-x0), float64(y1-y0))
if r.W < 8 || r.H < 8 {
o.cancel()
return
}
done := o.onDone
o.close()
if done != nil {
done(r)
}
}
// cancel 取消框选。
func (o *overlay) cancel() {
cancel := o.onCancel
o.close()
if cancel != nil {
cancel()
}
}
func (o *overlay) close() {
if o.closed {
return
}
o.closed = true
o.win.Close()
}
// catcher 是铺满窗口的事件接收层,负责鼠标拖拽。
type catcher struct {
widget.BaseWidget
o *overlay
}
func newCatcher(o *overlay) *catcher {
c := &catcher{o: o}
c.ExtendBaseWidget(c)
return c
}
func (c *catcher) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(canvas.NewRectangle(color.Transparent))
}
func (c *catcher) Dragged(e *fyne.DragEvent) {
if !c.o.dragging {
return
}
c.o.cur = e.Position
c.o.updateSelection()
}
func (c *catcher) DragEnd() {}
func (c *catcher) MouseDown(e *desktop.MouseEvent) {
c.o.dragging = true
c.o.start = e.Position
c.o.cur = e.Position
c.o.updateSelection()
}
func (c *catcher) MouseUp(e *desktop.MouseEvent) {
if !c.o.dragging {
return
}
c.o.dragging = false
c.o.cur = e.Position
c.o.finish()
}