latchshot 0.1.0

A lightweight yet intelligent window-aware screenshot tool
Documentation
# selection.rs 详解 —— 选择状态机

> 对应代码:`src/selection.rs`。本文件是纯逻辑模块:不依赖 Wayland、不依赖 niri,输入输出都只是数据。配套的审阅记录见 `docs/review/findings.md`
## 模块对外承诺什么

读代码从类型开始。这个模块只向外暴露三样东西:

```rust
pub enum Selection {
    Window(Rect),    // 点中了窗口 → 该窗口的全局几何
    Region(Rect),    // 拖拽选区 → 归一化后的矩形
}

pub enum SelectionResult {
    Selected(Selection),
    Cancelled,       // Esc 取消
}

pub struct Selector { ... }
```

`Window` 和 `Region` 目前下游处理相同(都取几何去裁切),但变体标签保留**用户意图**——将来窗口截图可以有不同命名/行为。`SelectionResult` 是"一次交互的终态"。

## 内部状态:机器有哪些状态、各携带什么

```rust
enum State {
    Waiting,                          // 光标未命中窗口、未按键
    Hover { pointer, target },        // 悬停:pointer=光标,target=命中的窗口下标
    Pressed { origin, target },       // 按下左键:origin=按下点,target=按下时锁定的窗口
    Dragging { origin, pointer },     // 拖动中:origin=起点,pointer=当前光标
}
```

两个关键设计:

1. **Pressed 同时记 origin 和 target**——release 时要靠它们区分"点击窗口"还是"开始拖拽";target 在按下瞬间锁定,之后光标在窗口内漂移不影响它。
2. **Dragging 故意不记 target**——进入拖拽就是自由选区,吸附概念退出舞台。

## Selector 本体

```rust
pub struct Selector {
    scene: Scene,
    state: State,
    drag_threshold_squared: f64,   // 阈值存平方:比较时免开方
}
```

- `scene`:选择要用的世界(窗口几何、遮挡顺序),构造时传入、此后只读;
- `drag_threshold_squared``new``drag_threshold.powi(2)` 预计算,`pointer_moved``distance_squared >= threshold_squared` 比较,省每次开方。

## 查询方法(不改状态,先读懂"现在是什么")

```rust
pub fn target_geometry(&self) -> Option<Rect> {
    let target = match self.state {
        State::Hover { target, .. } | State::Pressed { target, .. } => target,
        State::Waiting | State::Dragging { .. } => None,
    }?;

    Some(self.scene.windows[target].geometry)
}

pub fn region(&self) -> Option<Rect> {
    let State::Dragging { origin, pointer } = self.state else {
        return None;
    };

    Some(Rect::from_points(origin, pointer))
}
```

- `target_geometry`:只有 Hover/Pressed(吸附还活着)才可能有窗口目标;overlay 每帧拿它画高亮。
- `region`:只有 Dragging 才有选区;`Rect::from_points` 自动归一化方向(任意方向拖都得到合法矩形)。overlay 拖拽时每帧拿它画框。

## 状态转移核心:pointer_moved

所有移动事件汇到这一个方法,一次 match 完成全部转移:

```rust
pub fn pointer_moved(&mut self, pointer: Point) {
    self.state = match self.state {
        // 分支 1:没在按键——光标移动只是刷新悬停
        State::Waiting | State::Hover { .. } => State::Hover {
            pointer,
            target: self.target_at(pointer),
        },
        // 分支 2:按着且位移平方超过阈值 → 进入拖拽
        State::Pressed { origin, target: _ }
            if origin.distance_squared(pointer) >= self.drag_threshold_squared =>
        {
            State::Dragging { origin, pointer }
        }
        // 分支 3:按着但没超过阈值 → 保持 Pressed(防手抖)
        State::Pressed { origin, target } => State::Pressed { origin, target },
        // 分支 4:已在拖拽 → 只更新光标
        State::Dragging { origin, .. } => State::Dragging { origin, pointer },
    };
}
```

分支 2 的 `if` 是 **match guard**(附加条件):不满足就落到分支 3。注意分支 2 丢弃了 target(`target: _`)——进入拖拽后不再需要。

## 事件到结果的转换

```rust
pub const fn press(&mut self) {
    if let State::Hover { pointer, target } = self.state {
        self.state = State::Pressed {
            origin: pointer,
            target,
        };
    }
}
```

`press` 只在 Hover 时有效:按下点成为 origin,悬停命中的 target 被锁定。

`release` 是三种结局的分水岭:

```rust
pub fn release(&mut self) -> Option<SelectionResult> {
    let result = match self.state {
        // 结局 1:按下时有窗口目标 → 窗口选区(点击窗口)
        State::Pressed { target: Some(target), .. } => self.window_selection(target),
        // 结局 2:拖拽中且宽高非零 → 区域选区
        State::Dragging { origin, pointer } => {
            let region = Rect::from_points(origin, pointer);
            if region.width() == 0.0 || region.height() == 0.0 {
                self.state = State::Hover {
                    pointer,
                    target: self.target_at(pointer),
                };

                return None;
            }

            Selection::Region(region)
        }
        // 结局 3:按下时没有窗口目标(空点)→ 无结果
        State::Pressed { origin, target: None } => {
            self.state = State::Hover {
                pointer: origin,
                target: None,
            };

            return None;
        }
        State::Waiting | State::Hover { .. } => return None,
    };

    Some(SelectionResult::Selected(result))
}
```

重点:**所有"无结果"的分支都会把状态拨回 Hover**——失败的选择不留下烂状态,下一次事件仍能正常进入流程(测试 `releasing_a_line_restores_the_window_target` 钉死:划线 release 后 target 恢复)。

## 私有辅助

```rust
fn target_at(&self, point: Point) -> Option<usize> {
    self.scene.window_index_at(point)
}

fn window_selection(&self, index: usize) -> Selection {
    Selection::Window(self.scene.windows[index].geometry)
}
```

`window_index_at`(scene.rs)从前往后找第一个包含光标的窗口——前面的挡住后面的。

## 状态转移全图

```text
             pointer_moved(刷新悬停)
  Waiting ────────────────────→ Hover
      ↑                            │ press
      │ release(None)              ↓
      │                          Pressed ── release(有target) ──→ Selection::Window
      │                            │
      │                 pointer_moved ≥ 阈值
      │                            ↓
      └──────────────────────── Dragging ── release(非零面积) ──→ Selection::Region
                                   Esc(外部,session.rs)──→ SelectionResult::Cancelled
```

## 测试 ↔ 路径映射

每个测试对应转移图里的一条边:

| 测试 | 覆盖的路径 |
|---|---|
| `click_selects_the_snapped_window` | Hover→Press→release:窗口选区 |
| `dragging_past_the_threshold_selects_a_region` | 阈值内保持 Pressed;超阈值进 Dragging;release 得区域 |
| `releasing_an_empty_click_does_not_start_a_late_drag` | 空点 release 后,迟来的移动不会触发拖拽 |
| `a_line_is_not_a_region` | 水平线选区(宽 0)→ 不算区域 |
| `releasing_a_line_restores_the_window_target` | 划线 release 后状态回到 Hover,target 恢复 |

## 与调用方的关系

```text
overlay/session.rs
  PointerHandler   → pointer_moved / press / release(鼠标路径)
  KeyboardHandler  → Esc=Cancelled(写 result 字段)
main.rs
  → 消费 SelectionResult:Selected → 裁切;Cancelled → 退出
```

Selector 自己是纯逻辑:不碰 Wayland 事件类型,输入输出全是 `Point`/`Rect`/`SelectionResult`——所以 5 个测试全部在无图形环境跑。