616 lines
15 KiB
Go
616 lines
15 KiB
Go
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/layout"
|
||
"fyne.io/fyne/v2/theme"
|
||
"fyne.io/fyne/v2/widget"
|
||
|
||
"viewsagent/internal/capture"
|
||
"viewsagent/internal/win32"
|
||
)
|
||
|
||
// overlayTitle 是框选窗口标题,win32 需要靠它找到 HWND。
|
||
const overlayTitle = "viewsagent-overlay"
|
||
|
||
// 框选交互用到的尺寸(逻辑像素)。
|
||
const (
|
||
handleSize = 12 // 调整手柄的边长
|
||
edgeZone = 8 // 边缘的响应宽度
|
||
minSelect = 8 // 选区允许的最小边长
|
||
dragThresh = 5 // 判定为拖拽所需的最小位移
|
||
)
|
||
|
||
// dragMode 表示鼠标当前正在做什么。
|
||
type dragMode int
|
||
|
||
const (
|
||
modeNone dragMode = iota
|
||
modeCreate // 从空白处拖出新选区
|
||
modeMove // 拖动整个选区
|
||
modeResize // 拖动边缘或角上的手柄
|
||
)
|
||
|
||
// 八个手柄的编号,顺时针从左上角开始。
|
||
const (
|
||
hTopLeft = iota
|
||
hTopMid
|
||
hTopRight
|
||
hRightMid
|
||
hBottomRight
|
||
hBottomMid
|
||
hBottomLeft
|
||
hLeftMid
|
||
)
|
||
|
||
// rect 是逻辑坐标系下的矩形。
|
||
type rect struct {
|
||
x0, y0, x1, y1 float32
|
||
}
|
||
|
||
// w 返回宽度。
|
||
func (r rect) w() float32 { return r.x1 - r.x0 }
|
||
|
||
// h 返回高度。
|
||
func (r rect) h() float32 { return r.y1 - r.y0 }
|
||
|
||
// contains 判断点是否落在矩形内。
|
||
func (r rect) contains(x, y float32) bool {
|
||
return x >= r.x0 && x <= r.x1 && y >= r.y0 && y <= r.y1
|
||
}
|
||
|
||
// normalized 保证 x0<=x1 且 y0<=y1。
|
||
func (r rect) normalized() rect {
|
||
if r.x1 < r.x0 {
|
||
r.x0, r.x1 = r.x1, r.x0
|
||
}
|
||
if r.y1 < r.y0 {
|
||
r.y0, r.y1 = r.y1, r.y0
|
||
}
|
||
return r
|
||
}
|
||
|
||
// overlay 是一个铺满当前显示器的冻结帧框选层。
|
||
//
|
||
// 之所以不依赖窗口透明:Windows 下的 OpenGL 窗口透明并不可靠,
|
||
// 直接用"全屏截图做背景 + 遮罩"视觉等效,还能保证选区与截图完全对齐。
|
||
//
|
||
// 选区可以反复调整:空白处拖拽新建,选区内部拖拽移动,
|
||
// 边缘与角上的方块拖拽改大小,最后点「确定」或按 Enter 生效。
|
||
type overlay struct {
|
||
win fyne.Window
|
||
|
||
vd win32.Rect
|
||
mapping capture.Mapping
|
||
size fyne.Size
|
||
calibrated bool
|
||
initial capture.Region // 打开时预置的选区
|
||
|
||
bg *canvas.Image
|
||
dims [4]*canvas.Rectangle
|
||
edges [4]*canvas.Rectangle
|
||
handles [8]*canvas.Rectangle
|
||
hint *canvas.Text
|
||
bar *fyne.Container
|
||
tip *widget.Label
|
||
confirmBtn *widget.Button
|
||
cancelBtn *widget.Button
|
||
catcher *catcher
|
||
|
||
sel rect // 当前选区(逻辑坐标)
|
||
hasSel bool
|
||
|
||
mode dragMode
|
||
handle int
|
||
grab fyne.Position
|
||
orig rect
|
||
moved bool
|
||
|
||
closed bool
|
||
|
||
onDone func(capture.Region)
|
||
onCancel func()
|
||
}
|
||
|
||
// newOverlay 创建框选层。shot 应该是调用前抓好的整屏画面,
|
||
// initial 是上一次框选的区域,打开时直接带上方便调整。
|
||
func newOverlay(app fyne.App, shot image.Image, vd win32.Rect, initial capture.Region,
|
||
onDone func(capture.Region), onCancel func()) *overlay {
|
||
|
||
o := &overlay{vd: vd, initial: initial, 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])
|
||
}
|
||
for i := range o.handles {
|
||
hd := canvas.NewRectangle(lookupColor(theme.ColorNameForeground))
|
||
hd.StrokeColor = color.NRGBA{R: 0, G: 0, B: 0, A: 220}
|
||
hd.StrokeWidth = 1
|
||
o.handles[i] = hd
|
||
objs = append(objs, hd)
|
||
}
|
||
o.hint = canvas.NewText("拖拽框选区域", color.White)
|
||
o.hint.TextStyle = fyne.TextStyle{Bold: true}
|
||
o.hint.TextSize = 15
|
||
|
||
o.tip = widget.NewLabel("空白处拖拽新建 · 选区内拖动可移动 · 边缘白块可调整大小 · Enter 确定 · Esc 取消")
|
||
o.tip.Importance = widget.LowImportance
|
||
o.tip.Wrapping = fyne.TextWrapOff
|
||
|
||
o.confirmBtn = widget.NewButton("确定", func() { o.finish() })
|
||
o.cancelBtn = widget.NewButton("取消", func() { o.cancel() })
|
||
// 两侧留白让按钮居中,跟上面的提示文字对齐。
|
||
o.bar = container.NewVBox(o.tip,
|
||
container.NewHBox(layout.NewSpacer(), o.confirmBtn, o.cancelBtn, layout.NewSpacer()))
|
||
|
||
o.catcher = newCatcher(o)
|
||
objs = append(objs, o.catcher, o.hint, o.bar)
|
||
|
||
o.win.SetContent(container.NewWithoutLayout(objs...))
|
||
o.win.Canvas().SetOnTypedKey(func(k *fyne.KeyEvent) {
|
||
switch k.Name {
|
||
case fyne.KeyEscape:
|
||
o.cancel()
|
||
case fyne.KeyReturn, fyne.KeyEnter:
|
||
o.finish()
|
||
}
|
||
})
|
||
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
|
||
|
||
wr, err := windowRectByTitle(overlayTitle)
|
||
if err != nil {
|
||
mon, _ := win32.MonitorAtCursor()
|
||
wr = mon.Rect
|
||
}
|
||
if wr.Width() <= 0 || wr.Height() <= 0 {
|
||
return
|
||
}
|
||
|
||
o.mapping = capture.Mapping{
|
||
OffsetX: float64(wr.Left),
|
||
OffsetY: float64(wr.Top),
|
||
ScaleX: float64(wr.Width()) / float64(size.Width),
|
||
ScaleY: float64(wr.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)
|
||
|
||
// 底部工具条:确定 / 取消 + 操作提示。
|
||
barSize := o.bar.MinSize()
|
||
barX := (size.Width - barSize.Width) / 2
|
||
if barX < 8 {
|
||
barX = 8
|
||
}
|
||
barY := size.Height - barSize.Height - 16
|
||
if barY < 8 {
|
||
barY = 8
|
||
}
|
||
o.bar.Move(fyne.NewPos(barX, barY))
|
||
o.bar.Resize(barSize)
|
||
|
||
o.calibrated = true
|
||
|
||
// 预置上一次的选区,打开就能直接调整。
|
||
if o.initial.Valid() {
|
||
lx, ly, lw, lh := o.mapping.ToLogical(o.initial)
|
||
r := rect{
|
||
x0: float32(lx), y0: float32(ly),
|
||
x1: float32(lx + lw), y1: float32(ly + lh),
|
||
}.normalized()
|
||
// 夹回窗口内(选区可能在别的显示器上)。
|
||
r.x0 = max(r.x0, 0)
|
||
r.y0 = max(r.y0, 0)
|
||
r.x1 = min(r.x1, size.Width)
|
||
r.y1 = min(r.y1, size.Height)
|
||
if r.w() >= minSelect && r.h() >= minSelect {
|
||
o.sel = r
|
||
o.hasSel = 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)
|
||
}
|
||
|
||
// ---------- 交互 ----------
|
||
|
||
// beginDrag 按下鼠标,按落点决定这次是新建、移动还是调整大小。
|
||
func (o *overlay) beginDrag(pos fyne.Position) {
|
||
if !o.calibrated {
|
||
return
|
||
}
|
||
o.mode, o.handle = o.hitTest(pos.X, pos.Y)
|
||
o.grab = pos
|
||
o.orig = o.sel
|
||
o.moved = false
|
||
}
|
||
|
||
// updateDrag 拖动过程中更新选区。
|
||
func (o *overlay) updateDrag(pos fyne.Position) {
|
||
if o.mode == modeNone {
|
||
return
|
||
}
|
||
if abs32(pos.X-o.grab.X) > dragThresh || abs32(pos.Y-o.grab.Y) > dragThresh {
|
||
o.moved = true
|
||
}
|
||
|
||
switch o.mode {
|
||
case modeCreate:
|
||
if !o.moved {
|
||
// 只是点了一下,不动原来的选区。
|
||
return
|
||
}
|
||
o.sel = rect{o.grab.X, o.grab.Y, pos.X, pos.Y}.normalized()
|
||
case modeMove:
|
||
o.sel = o.movedRect(pos)
|
||
case modeResize:
|
||
o.sel = o.resizedRect(pos)
|
||
}
|
||
o.hasSel = true
|
||
o.updateSelection()
|
||
}
|
||
|
||
// endDrag 松开鼠标。选区会留在界面上等用户确认,
|
||
// 这样才能反复微调边界,而不是一松手就定死。
|
||
func (o *overlay) endDrag(pos fyne.Position) {
|
||
if o.mode == modeNone {
|
||
return
|
||
}
|
||
o.updateDrag(pos)
|
||
o.mode = modeNone
|
||
o.handle = -1
|
||
}
|
||
|
||
// hitTest 判断鼠标落点对应哪种拖拽方式,以及命中的手柄编号。
|
||
func (o *overlay) hitTest(x, y float32) (dragMode, int) {
|
||
if !o.hasSel {
|
||
return modeCreate, -1
|
||
}
|
||
r := o.sel
|
||
half := float32(handleSize)
|
||
|
||
// 四个角上的手柄优先命中。
|
||
corners := [4][2]float32{
|
||
{r.x0, r.y0}, {r.x1, r.y0}, {r.x1, r.y1}, {r.x0, r.y1},
|
||
}
|
||
cornerHandles := [4]int{hTopLeft, hTopRight, hBottomRight, hBottomLeft}
|
||
for i, c := range corners {
|
||
if abs32(x-c[0]) <= half && abs32(y-c[1]) <= half {
|
||
return modeResize, cornerHandles[i]
|
||
}
|
||
}
|
||
|
||
// 四条边中点上的手柄。
|
||
mx, my := (r.x0+r.x1)/2, (r.y0+r.y1)/2
|
||
mids := [4][3]float32{
|
||
{mx, r.y0, hTopMid}, {r.x1, my, hRightMid},
|
||
{mx, r.y1, hBottomMid}, {r.x0, my, hLeftMid},
|
||
}
|
||
for _, m := range mids {
|
||
if abs32(x-m[0]) <= half && abs32(y-m[1]) <= half {
|
||
return modeResize, int(m[2])
|
||
}
|
||
}
|
||
|
||
// 边上的其他位置也能拖动改大小。
|
||
ez := float32(edgeZone)
|
||
if abs32(x-r.x0) <= ez && y > r.y0+ez && y < r.y1-ez {
|
||
return modeResize, hLeftMid
|
||
}
|
||
if abs32(x-r.x1) <= ez && y > r.y0+ez && y < r.y1-ez {
|
||
return modeResize, hRightMid
|
||
}
|
||
if abs32(y-r.y0) <= ez && x > r.x0+ez && x < r.x1-ez {
|
||
return modeResize, hTopMid
|
||
}
|
||
if abs32(y-r.y1) <= ez && x > r.x0+ez && x < r.x1-ez {
|
||
return modeResize, hBottomMid
|
||
}
|
||
|
||
// 选区内部是整体移动。
|
||
if r.contains(x, y) {
|
||
return modeMove, -1
|
||
}
|
||
return modeCreate, -1
|
||
}
|
||
|
||
// movedRect 计算平移后的选区,并夹回窗口范围内。
|
||
func (o *overlay) movedRect(pos fyne.Position) rect {
|
||
r := o.orig
|
||
dx, dy := pos.X-o.grab.X, pos.Y-o.grab.Y
|
||
r.x0 += dx
|
||
r.x1 += dx
|
||
r.y0 += dy
|
||
r.y1 += dy
|
||
|
||
if r.x0 < 0 {
|
||
r.x1 -= r.x0
|
||
r.x0 = 0
|
||
}
|
||
if r.y0 < 0 {
|
||
r.y1 -= r.y0
|
||
r.y0 = 0
|
||
}
|
||
if r.x1 > o.size.Width {
|
||
r.x0 -= r.x1 - o.size.Width
|
||
r.x1 = o.size.Width
|
||
}
|
||
if r.y1 > o.size.Height {
|
||
r.y0 -= r.y1 - o.size.Height
|
||
r.y1 = o.size.Height
|
||
}
|
||
return r
|
||
}
|
||
|
||
// resizedRect 按手柄算出新的选区,边不会被拖翻过去。
|
||
func (o *overlay) resizedRect(pos fyne.Position) rect {
|
||
r := o.orig
|
||
x, y := pos.X, pos.Y
|
||
minW := float32(minSelect)
|
||
minH := float32(minSelect)
|
||
|
||
switch o.handle {
|
||
case hTopLeft:
|
||
r.x0 = min(x, r.x1-minW)
|
||
r.y0 = min(y, r.y1-minH)
|
||
case hTopMid:
|
||
r.y0 = min(y, r.y1-minH)
|
||
case hTopRight:
|
||
r.x1 = max(x, r.x0+minW)
|
||
r.y0 = min(y, r.y1-minH)
|
||
case hRightMid:
|
||
r.x1 = max(x, r.x0+minW)
|
||
case hBottomRight:
|
||
r.x1 = max(x, r.x0+minW)
|
||
r.y1 = max(y, r.y0+minH)
|
||
case hBottomMid:
|
||
r.y1 = max(y, r.y0+minH)
|
||
case hBottomLeft:
|
||
r.x0 = min(x, r.x1-minW)
|
||
r.y1 = max(y, r.y0+minH)
|
||
case hLeftMid:
|
||
r.x0 = min(x, r.x1-minW)
|
||
}
|
||
return r
|
||
}
|
||
|
||
// handleRects 返回八个手柄的矩形。
|
||
func (o *overlay) handleRects() [8]rect {
|
||
r := o.sel
|
||
h := float32(handleSize) / 2
|
||
mx, my := (r.x0+r.x1)/2, (r.y0+r.y1)/2
|
||
center := func(x, y float32) rect {
|
||
return rect{x0: x - h, y0: y - h, x1: x + h, y1: y + h}
|
||
}
|
||
return [8]rect{
|
||
center(r.x0, r.y0), // 左上
|
||
center(mx, r.y0), // 上中
|
||
center(r.x1, r.y0), // 右上
|
||
center(r.x1, my), // 右中
|
||
center(r.x1, r.y1), // 右下
|
||
center(mx, r.y1), // 下中
|
||
center(r.x0, r.y1), // 左下
|
||
center(r.x0, my), // 左中
|
||
}
|
||
}
|
||
|
||
// abs32 返回绝对值。
|
||
func abs32(v float32) float32 {
|
||
if v < 0 {
|
||
return -v
|
||
}
|
||
return v
|
||
}
|
||
|
||
// ---------- 绘制 ----------
|
||
|
||
// updateSelection 重排遮罩、边框、手柄与尺寸提示。
|
||
func (o *overlay) updateSelection() {
|
||
if o.size.Width <= 0 {
|
||
return
|
||
}
|
||
w, h := o.size.Width, o.size.Height
|
||
if !o.hasSel || o.sel.w() < 1 || o.sel.h() < 1 {
|
||
// 尚未框选:整屏压暗
|
||
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))
|
||
}
|
||
for _, hd := range o.handles {
|
||
hd.Resize(fyne.NewSize(0, 0))
|
||
}
|
||
o.hint.Text = "拖拽框选区域 · Enter 确定 · Esc 取消"
|
||
o.hint.Move(fyne.NewPos(theme.Padding()*2, theme.Padding()*2))
|
||
o.hint.Refresh()
|
||
return
|
||
}
|
||
|
||
r := o.sel
|
||
// 上下左右四块压暗区域,挖出选区
|
||
o.dims[0].Move(fyne.NewPos(0, 0))
|
||
o.dims[0].Resize(fyne.NewSize(w, r.y0))
|
||
o.dims[1].Move(fyne.NewPos(0, r.y1))
|
||
o.dims[1].Resize(fyne.NewSize(w, h-r.y1))
|
||
o.dims[2].Move(fyne.NewPos(0, r.y0))
|
||
o.dims[2].Resize(fyne.NewSize(r.x0, r.h()))
|
||
o.dims[3].Move(fyne.NewPos(r.x1, r.y0))
|
||
o.dims[3].Resize(fyne.NewSize(w-r.x1, r.h()))
|
||
|
||
// 选区描边
|
||
const t = 2
|
||
o.edges[0].Move(fyne.NewPos(r.x0, r.y0))
|
||
o.edges[0].Resize(fyne.NewSize(r.w(), t))
|
||
o.edges[1].Move(fyne.NewPos(r.x0, r.y1-t))
|
||
o.edges[1].Resize(fyne.NewSize(r.w(), t))
|
||
o.edges[2].Move(fyne.NewPos(r.x0, r.y0))
|
||
o.edges[2].Resize(fyne.NewSize(t, r.h()))
|
||
o.edges[3].Move(fyne.NewPos(r.x1-t, r.y0))
|
||
o.edges[3].Resize(fyne.NewSize(t, r.h()))
|
||
|
||
// 八个调整手柄,提示边界可以拖
|
||
for i, hr := range o.handleRects() {
|
||
o.handles[i].Move(fyne.NewPos(hr.x0, hr.y0))
|
||
o.handles[i].Resize(fyne.NewSize(hr.w(), hr.h()))
|
||
}
|
||
|
||
phys := o.mapping.ToPhysical(float64(r.x0), float64(r.y0), float64(r.w()), float64(r.h()))
|
||
o.hint.Text = fmt.Sprintf("%d × %d", phys.W, phys.H)
|
||
hy := r.y0 - 26
|
||
if hy < 4 {
|
||
hy = r.y0 + 6
|
||
}
|
||
o.hint.Move(fyne.NewPos(r.x0+6, hy))
|
||
o.hint.Refresh()
|
||
}
|
||
|
||
// ---------- 确认与取消 ----------
|
||
|
||
// finish 确认选区。
|
||
func (o *overlay) finish() {
|
||
if !o.calibrated {
|
||
o.calibrate()
|
||
}
|
||
if !o.hasSel {
|
||
return
|
||
}
|
||
r := o.sel
|
||
if r.w() < minSelect || r.h() < minSelect {
|
||
o.cancel()
|
||
return
|
||
}
|
||
phys := o.mapping.ToPhysical(float64(r.x0), float64(r.y0), float64(r.w()), float64(r.h()))
|
||
if phys.W < 8 || phys.H < 8 {
|
||
o.cancel()
|
||
return
|
||
}
|
||
done := o.onDone
|
||
o.close()
|
||
if done != nil {
|
||
done(phys)
|
||
}
|
||
}
|
||
|
||
// 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))
|
||
}
|
||
|
||
// MouseDown 按下鼠标:右键取消,左键按落点决定拖拽方式。
|
||
func (c *catcher) MouseDown(e *desktop.MouseEvent) {
|
||
if e.Button == desktop.MouseButtonSecondary {
|
||
c.o.cancel()
|
||
return
|
||
}
|
||
c.o.beginDrag(e.Position)
|
||
}
|
||
|
||
// Dragged 拖动中,实时更新选区。
|
||
func (c *catcher) Dragged(e *fyne.DragEvent) {
|
||
c.o.updateDrag(e.Position)
|
||
}
|
||
|
||
// DragEnd 松开鼠标。
|
||
func (c *catcher) DragEnd(e *fyne.DragEvent) {
|
||
c.o.endDrag(e.Position)
|
||
}
|
||
|
||
// MouseUp 松开鼠标(没拖动时也会走到这里)。
|
||
func (c *catcher) MouseUp(e *desktop.MouseEvent) {
|
||
c.o.endDrag(e.Position)
|
||
}
|
||
|
||
// DoubleTapped 双击选区内部,直接确认。
|
||
func (c *catcher) DoubleTapped(e *fyne.PointEvent) {
|
||
c.o.finish()
|
||
}
|