first commit

This commit is contained in:
hero920103 committed 2026-10-27 17:58:28 +08:00
commit 3553790475
27 files changed
+3839

No files matched your search

+299
View File
@@ -0,0 +1,299 @@
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(color.NRGBA{R: 0x5A, G: 0xC8, B: 0xFF, A: 0xFF})
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()
}