use crate::capture::{Capture, XCapCapture};
use crate::color::{self, ColorBlob, ColorSpec};
use crate::frame::{Frame, Rect};
use crate::matcher::{Match, Matcher, RgbMatcher};
use crate::template::Template;
use crate::Result;
use std::time::{Duration, Instant};
#[cfg(all(windows, feature = "capture-dxgi"))]
use crate::Error;
#[cfg(feature = "match-corr")]
use crate::matcher_corr::{CorrConfig, CorrMatcher};
pub enum CaptureKind {
Monitor,
#[cfg(all(windows, feature = "capture-gdi"))]
Gdi,
#[cfg(all(windows, feature = "capture-dxgi"))]
Dxgi,
#[cfg(all(windows, feature = "capture-window"))]
Window(crate::capture_window::WindowHandle),
#[cfg(all(windows, feature = "capture-window"))]
WindowByTitle(String),
#[cfg(all(windows, any(feature = "capture-gdi", feature = "capture-dxgi")))]
Auto,
}
pub enum MatchKind {
Rgb { tolerance: i32 },
#[cfg(feature = "match-corr")]
Corr,
#[cfg(feature = "match-corr")]
CorrWith(CorrConfig),
}
pub struct Finder {
capture: Box<dyn Capture>,
matcher: Box<dyn Matcher>,
region: Option<Rect>,
frame: Frame,
prev_frame: Option<Frame>,
cache_key: Option<u64>,
cache_result: Option<Option<Match>>,
region_offscreen_warned: bool,
}
impl Finder {
pub fn builder() -> FinderBuilder {
FinderBuilder {
capture: None,
matcher: None,
region: None,
}
}
pub fn new(capture: Box<dyn Capture>, matcher: Box<dyn Matcher>) -> Self {
Finder {
capture,
matcher,
region: None,
frame: Frame::bgra8(0, 0, Vec::new()),
prev_frame: None,
cache_key: None,
cache_result: None,
region_offscreen_warned: false,
}
}
pub fn find_on_screen(&mut self, tpl: &Template) -> Result<Option<Match>> {
let _t0 = px_timer!();
let (changed, origin) = self.grab_scoped()?;
let key = self.key_of(tpl);
if !changed && self.cache_key == Some(key) {
if let Some(prev) = self.cache_result {
px_trace!(
op = "find_on_screen",
backend = self.capture.backend(),
cache_hit = true,
hit = prev.is_some(),
elapsed_us = _t0.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0),
);
return Ok(prev);
}
}
let m = self.search(tpl, origin);
self.cache_key = Some(key);
self.cache_result = Some(m);
px_trace!(
op = "find_on_screen",
backend = self.capture.backend(),
cache_hit = false,
changed,
frame = format!("{}x{}", self.frame.width, self.frame.height),
tpl_key = format!("{:016x}", key),
hit = m.is_some(),
elapsed_us = _t0.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0),
);
Ok(m)
}
pub fn find_color_on_screen(
&mut self,
spec: &ColorSpec,
min_area: usize,
) -> Result<Vec<ColorBlob>> {
let _t0 = px_timer!();
let (_, origin) = self.grab_scoped()?;
let region = self.search_rect(origin);
let mut blobs = color::find_blobs(&self.frame, spec, region, min_area);
if origin != (0, 0) {
for b in &mut blobs {
b.bounds.x += origin.0 as usize;
b.bounds.y += origin.1 as usize;
}
}
px_trace!(
op = "find_color_on_screen",
backend = self.capture.backend(),
color = format!("{:?}±{}", spec.rgb, spec.tolerance),
region = format!(
"({},{},{}x{})",
region.x, region.y, region.width, region.height
),
blobs = blobs.len(),
elapsed_us = _t0.map(|t| t.elapsed().as_micros() as u64).unwrap_or(0),
);
Ok(blobs)
}
pub fn find_until(
&mut self,
tpl: &Template,
timeout: Duration,
interval: Duration,
) -> Result<Option<Match>> {
let m = self.poll(tpl, timeout, interval, true)?;
px_trace!(
op = "find_until",
result = if m.is_some() { "hit" } else { "timeout" },
timeout = ?timeout,
);
Ok(m)
}
pub fn wait_gone(
&mut self,
tpl: &Template,
timeout: Duration,
interval: Duration,
) -> Result<bool> {
let hit = self.poll(tpl, timeout, interval, false)?;
px_trace!(
op = "wait_gone",
result = if hit.is_none() { "gone" } else { "still_present" },
timeout = ?timeout,
);
Ok(hit.is_none())
}
fn poll(
&mut self,
tpl: &Template,
timeout: Duration,
interval: Duration,
want_hit: bool,
) -> Result<Option<Match>> {
let deadline = Instant::now().checked_add(timeout);
loop {
let m = self.find_on_screen(tpl)?;
if m.is_some() == want_hit {
return Ok(m);
}
let now = Instant::now();
if let Some(deadline) = deadline {
if now >= deadline {
return Ok(m);
}
std::thread::sleep(interval.min(deadline - now));
} else {
std::thread::sleep(interval);
}
}
}
pub fn find_all_on_screen(&mut self, tpl: &Template, max: usize) -> Result<Vec<Match>> {
let (_, origin) = self.grab_scoped()?;
let region = self.search_rect(origin);
let mut ms = self.matcher.find_all(&self.frame, tpl, region, max);
if origin != (0, 0) {
for m in &mut ms {
m.x += origin.0;
m.y += origin.1;
}
}
Ok(ms)
}
pub fn find_many_on_screen(&mut self, tpls: &[&Template]) -> Result<Vec<Option<Match>>> {
let (_, origin) = self.grab_scoped()?;
Ok(tpls.iter().map(|t| self.search(t, origin)).collect())
}
pub fn find_in_frame(&self, frame: &Frame, tpl: &Template) -> Option<Match> {
match self.region {
Some(r) => self.matcher.find_in(frame, tpl, r),
None => self.matcher.find(frame, tpl),
}
}
pub fn find_center_on_screen(&mut self, tpl: &Template) -> Result<Option<Match>> {
let m = self.find_on_screen(tpl)?;
Ok(m.map(|m| {
let (x, y) = m.center(tpl);
Match {
x,
y,
score: m.score,
}
}))
}
pub fn diff_since_last(&mut self, rect: Rect) -> Result<u32> {
let _ = self.grab_scoped()?;
let r = self.frame.clamp(rect);
let count = match self.prev_frame.as_ref() {
Some(prev)
if prev.width == self.frame.width
&& prev.height == self.frame.height
&& prev.format == self.frame.format =>
{
let (ro, go, bo) = self.frame.rgb_offsets();
let (pro, pgo, pbo) = prev.rgb_offsets();
let sw4 = self.frame.width * 4;
let pw4 = prev.width * 4;
let mut diff = 0u32;
for y in r.y..r.y + r.height {
for x in r.x..r.x + r.width {
let ci = y * sw4 + x * 4;
let pi = y * pw4 + x * 4;
if self.frame.pixels[ci + ro] != prev.pixels[pi + pro]
|| self.frame.pixels[ci + go] != prev.pixels[pi + pgo]
|| self.frame.pixels[ci + bo] != prev.pixels[pi + pbo]
{
diff += 1;
}
}
}
diff
}
_ => (r.width * r.height) as u32,
};
match self.prev_frame.as_mut() {
Some(prev) if prev.pixels.len() == self.frame.pixels.len() => {
prev.pixels.copy_from_slice(&self.frame.pixels);
prev.width = self.frame.width;
prev.height = self.frame.height;
prev.format = self.frame.format;
}
_ => self.prev_frame = Some(self.frame.clone()),
}
Ok(count)
}
pub fn region(&self) -> Option<Rect> {
self.region
}
fn grab_scoped(&mut self) -> Result<(bool, (i32, i32))> {
if let Some(r) = self.region {
if let Some(f) = self.capture.grab_region(r)? {
let changed = frame_differs(&self.frame, &f);
if changed {
self.frame = f;
}
self.warn_if_region_offscreen();
return Ok((changed, (r.x as i32, r.y as i32)));
}
}
let changed = self.capture.grab_into(&mut self.frame)?;
self.warn_if_region_offscreen();
Ok((changed, (0, 0)))
}
fn warn_if_region_offscreen(&mut self) {
let Some(r) = self.region else {
return;
};
if self.frame.width == 0 || self.frame.height == 0 {
return; }
let c = self.frame.clamp(r);
if (c.width != 0 && c.height != 0) || self.region_offscreen_warned {
return;
}
self.region_offscreen_warned = true;
px_warn!(
region = format!("({},{},{}x{})", r.x, r.y, r.width, r.height),
frame = format!("{}x{}", self.frame.width, self.frame.height),
backend = self.capture.backend(),
"region 与抓到的画面完全不相交,该区域不会有任何命中:检查坐标是否写错,或目标是否在另一块显示器上",
);
}
fn search_rect(&self, origin: (i32, i32)) -> Rect {
if origin == (0, 0) {
self.region.unwrap_or_else(|| self.frame.full_rect())
} else {
self.frame.full_rect()
}
}
fn search(&self, tpl: &Template, origin: (i32, i32)) -> Option<Match> {
let mut m = if origin != (0, 0) {
self.matcher.find(&self.frame, tpl)?
} else {
match self.region {
Some(r) => self.matcher.find_in(&self.frame, tpl, r)?,
None => self.matcher.find(&self.frame, tpl)?,
}
};
m.x += origin.0;
m.y += origin.1;
Some(m)
}
pub fn set_region(&mut self, region: Option<Rect>) {
self.region = region;
self.cache_key = None;
self.cache_result = None;
self.region_offscreen_warned = false;
}
fn key_of(&self, tpl: &Template) -> u64 {
let rk = match self.region {
None => 0u64,
Some(r) => {
(r.x as u64).wrapping_mul(1000003)
^ (r.y as u64).wrapping_mul(7349287)
^ (r.width as u64).wrapping_mul(911)
^ (r.height as u64)
}
};
tpl.content_key() ^ rk.rotate_left(32)
}
}
fn frame_differs(prev: &Frame, next: &Frame) -> bool {
prev.width != next.width
|| prev.height != next.height
|| prev.format != next.format
|| prev.pixels != next.pixels
}
pub struct FinderBuilder {
capture: Option<CaptureKind>,
matcher: Option<MatchKind>,
region: Option<Rect>,
}
impl FinderBuilder {
pub fn capture(mut self, k: CaptureKind) -> Self {
self.capture = Some(k);
self
}
pub fn matcher(mut self, k: MatchKind) -> Self {
self.matcher = Some(k);
self
}
pub fn region(mut self, r: impl Into<Rect>) -> Self {
self.region = Some(r.into());
self
}
pub fn build(self) -> Result<Finder> {
let capture: Box<dyn Capture> = match self.capture.unwrap_or(CaptureKind::Monitor) {
CaptureKind::Monitor => Box::new(XCapCapture::primary()?),
#[cfg(all(windows, feature = "capture-gdi"))]
CaptureKind::Gdi => Box::new(crate::capture_gdi::GdiCapture::new_primary()),
#[cfg(all(windows, feature = "capture-dxgi"))]
CaptureKind::Dxgi => Box::new(
crate::capture_dxgi::DxgiCapture::new_primary()
.ok_or_else(|| Error::capture("DXGI desktop duplication unavailable"))?,
),
#[cfg(all(windows, feature = "capture-window"))]
CaptureKind::Window(h) => Box::new(crate::capture_window::WindowCapture::new(h)),
#[cfg(all(windows, feature = "capture-window"))]
CaptureKind::WindowByTitle(ref title) => {
Box::new(crate::capture_window::WindowCapture::from_title(title)?)
}
#[cfg(all(windows, any(feature = "capture-gdi", feature = "capture-dxgi")))]
CaptureKind::Auto => auto_capture()?,
};
let matcher: Box<dyn Matcher> =
match self.matcher.unwrap_or(MatchKind::Rgb { tolerance: 25 }) {
MatchKind::Rgb { tolerance } => Box::new(RgbMatcher::new(tolerance)),
#[cfg(feature = "match-corr")]
MatchKind::Corr => Box::new(CorrMatcher::new()),
#[cfg(feature = "match-corr")]
MatchKind::CorrWith(cfg) => Box::new(CorrMatcher::with_config(cfg)),
};
let mut f = Finder::new(capture, matcher);
f.region = self.region;
Ok(f)
}
}
#[cfg(all(windows, any(feature = "capture-gdi", feature = "capture-dxgi")))]
fn auto_capture() -> Result<Box<dyn Capture>> {
#[cfg(feature = "capture-dxgi")]
if let Some(c) = crate::capture_dxgi::DxgiCapture::new_primary() {
return Ok(Box::new(c));
}
#[cfg(feature = "capture-gdi")]
{
return Ok(Box::new(crate::capture_gdi::GdiCapture::new_primary()));
}
#[allow(unreachable_code)]
Ok(Box::new(XCapCapture::primary()?))
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::rc::Rc;
const W: usize = 32;
const H: usize = 32;
fn frame(with_target: bool) -> Frame {
let mut px = vec![0u8; W * H * 4];
if with_target {
for y in 10..18 {
for x in 10..18 {
let i = (y * W + x) * 4;
px[i] = 255;
px[i + 1] = 0;
px[i + 2] = 0;
px[i + 3] = 255;
}
}
}
Frame::rgba8(W, H, px)
}
struct SeqCapture {
present: Vec<bool>,
grabs: usize,
}
impl Capture for SeqCapture {
fn grab(&mut self) -> Result<Frame> {
let i = self.grabs.min(self.present.len() - 1);
self.grabs += 1;
Ok(frame(self.present[i]))
}
fn backend(&self) -> &'static str {
"mock"
}
}
fn target_tpl() -> Template {
Template::from_rgb([255, 0, 0].repeat(8 * 8), 8, 8)
}
fn finder_with(present: Vec<bool>) -> Finder {
Finder::new(
Box::new(SeqCapture { present, grabs: 0 }),
Box::new(RgbMatcher::new(0)),
)
}
#[test]
fn find_until_hits_after_a_few_misses() {
let mut f = finder_with(vec![false, false, true]);
let m = f
.find_until(
&target_tpl(),
Duration::from_secs(5),
Duration::from_millis(2),
)
.expect("mock 不会出错")
.expect("第 3 帧应命中");
assert_eq!((m.x, m.y), (10, 10));
}
#[test]
fn find_until_times_out() {
let mut f = finder_with(vec![false]);
let m = f
.find_until(
&target_tpl(),
Duration::from_millis(30),
Duration::from_millis(5),
)
.expect("mock 不会出错");
assert!(m.is_none(), "一直找不到应在超时后返回 None");
}
#[test]
fn wait_gone_returns_true_and_false() {
let mut f = finder_with(vec![true, true, false]);
assert!(f
.wait_gone(
&target_tpl(),
Duration::from_secs(5),
Duration::from_millis(2)
)
.unwrap());
let mut f2 = finder_with(vec![true]);
assert!(!f2
.wait_gone(
&target_tpl(),
Duration::from_millis(30),
Duration::from_millis(5)
)
.unwrap());
}
#[test]
fn find_color_on_screen_high_level_entry() {
let mut f = finder_with(vec![true]);
let blobs = f
.find_color_on_screen(&ColorSpec::new(255, 0, 0, 10), 32)
.unwrap();
assert_eq!(blobs.len(), 1);
assert_eq!(blobs[0].bounds, Rect::new(10, 10, 8, 8));
assert_eq!(blobs[0].area, 64);
f.set_region(Some(Rect::new(0, 20, 32, 12)));
assert!(f
.find_color_on_screen(&ColorSpec::new(255, 0, 0, 10), 1)
.unwrap()
.is_empty());
}
fn region_frame() -> Frame {
let (w, h) = (24usize, 24usize);
let mut px = vec![0u8; w * h * 4];
for y in 5..13 {
for x in 4..12 {
let i = (y * w + x) * 4;
px[i] = 255;
px[i + 3] = 255;
}
}
Frame::rgba8(w, h, px)
}
struct RegionCap {
region_grabs: Rc<Cell<usize>>,
full_grabs: Rc<Cell<usize>>,
}
impl Capture for RegionCap {
fn grab(&mut self) -> Result<Frame> {
self.full_grabs.set(self.full_grabs.get() + 1);
Ok(frame(false))
}
fn grab_region(&mut self, r: Rect) -> Result<Option<Frame>> {
assert_eq!(
(r.x, r.y, r.width, r.height),
(20, 16, 24, 24),
"应收到屏幕绝对坐标区域"
);
self.region_grabs.set(self.region_grabs.get() + 1);
Ok(Some(region_frame()))
}
fn backend(&self) -> &'static str {
"mock-region"
}
}
fn region_finder() -> (Finder, Rc<Cell<usize>>, Rc<Cell<usize>>) {
let (rg, fg) = (Rc::new(Cell::new(0)), Rc::new(Cell::new(0)));
let mut f = Finder::new(
Box::new(RegionCap {
region_grabs: rg.clone(),
full_grabs: fg.clone(),
}),
Box::new(RgbMatcher::new(0)),
);
f.set_region(Some(Rect::new(20, 16, 24, 24)));
(f, rg, fg)
}
#[test]
fn region_prefers_backend_region_grab() {
let (mut f, rg, fg) = region_finder();
let m = f
.find_on_screen(&target_tpl())
.unwrap()
.expect("区域内应命中");
assert_eq!((m.x, m.y), (24, 21), "应返回屏幕绝对坐标");
assert_eq!(rg.get(), 1);
assert_eq!(fg.get(), 0, "能只截区域时不应再截全屏");
}
#[test]
fn region_grab_shifts_all_and_color_results() {
let (mut f, rg, _) = region_finder();
let all = f.find_all_on_screen(&target_tpl(), 0).unwrap();
assert_eq!(all.len(), 1);
assert_eq!((all[0].x, all[0].y), (24, 21));
let blobs = f
.find_color_on_screen(&ColorSpec::new(255, 0, 0, 10), 32)
.unwrap();
assert_eq!(blobs.len(), 1);
assert_eq!(blobs[0].bounds, Rect::new(24, 21, 8, 8));
assert_eq!(blobs[0].area, 64);
assert_eq!(rg.get(), 2, "两次入口都应走区域路径");
}
struct SpyMatcher {
finds: Rc<Cell<usize>>,
find_ins: Rc<Cell<usize>>,
}
impl Matcher for SpyMatcher {
fn find(&self, _frame: &Frame, _tpl: &Template) -> Option<Match> {
self.finds.set(self.finds.get() + 1);
Some(Match {
x: 1,
y: 2,
score: 1.0,
})
}
fn find_in(&self, _frame: &Frame, _tpl: &Template, _region: Rect) -> Option<Match> {
self.find_ins.set(self.find_ins.get() + 1);
Some(Match {
x: 1,
y: 2,
score: 1.0,
})
}
}
fn spy() -> (SpyMatcher, Rc<Cell<usize>>, Rc<Cell<usize>>) {
let (f, i) = (Rc::new(Cell::new(0)), Rc::new(Cell::new(0)));
(
SpyMatcher {
finds: f.clone(),
find_ins: i.clone(),
},
f,
i,
)
}
#[test]
fn no_region_uses_find_not_find_in() {
let (m, finds, find_ins) = spy();
let mut f = Finder::new(
Box::new(SeqCapture {
present: vec![true],
grabs: 0,
}),
Box::new(m),
);
assert!(f.find_on_screen(&target_tpl()).unwrap().is_some());
assert_eq!((finds.get(), find_ins.get()), (1, 0));
}
#[test]
fn region_without_backend_support_uses_find_in() {
let (m, finds, find_ins) = spy();
let mut f = Finder::new(
Box::new(SeqCapture {
present: vec![true],
grabs: 0,
}),
Box::new(m),
);
f.set_region(Some(Rect::new(2, 3, 8, 8)));
assert!(f.find_on_screen(&target_tpl()).unwrap().is_some());
assert_eq!((finds.get(), find_ins.get()), (0, 1));
}
#[test]
fn region_grab_searches_whole_frame_with_find() {
let (m, finds, find_ins) = spy();
let mut f = Finder::new(
Box::new(RegionCap {
region_grabs: Rc::new(Cell::new(0)),
full_grabs: Rc::new(Cell::new(0)),
}),
Box::new(m),
);
f.set_region(Some(Rect::new(20, 16, 24, 24)));
let hit = f.find_on_screen(&target_tpl()).unwrap().unwrap();
assert_eq!((hit.x, hit.y), (21, 18));
assert_eq!((finds.get(), find_ins.get()), (1, 0));
}
struct StaticRegion;
impl Capture for StaticRegion {
fn grab(&mut self) -> Result<Frame> {
Ok(Frame::bgra8(32, 32, vec![9u8; 32 * 32 * 4]))
}
fn grab_region(&mut self, r: Rect) -> Result<Option<Frame>> {
Ok(Some(Frame::bgra8(
r.width,
r.height,
vec![9u8; r.width * r.height * 4],
)))
}
fn backend(&self) -> &'static str {
"mock-static-region"
}
}
#[test]
fn region_grab_still_uses_static_frame_cache() {
let (m, finds, find_ins) = spy();
let mut f = Finder::new(Box::new(StaticRegion), Box::new(m));
f.set_region(Some(Rect::new(4, 4, 24, 24)));
assert!(f.find_on_screen(&target_tpl()).unwrap().is_some());
assert!(f.find_on_screen(&target_tpl()).unwrap().is_some());
assert!(f.find_on_screen(&target_tpl()).unwrap().is_some());
assert_eq!(
(finds.get(), find_ins.get()),
(1, 0),
"画面逐字节未变,后两次应复用缓存"
);
}
#[test]
fn region_grab_researches_when_pixels_change() {
struct TogglingRegion {
n: usize,
}
impl Capture for TogglingRegion {
fn grab(&mut self) -> Result<Frame> {
Ok(Frame::bgra8(24, 24, vec![9u8; 24 * 24 * 4]))
}
fn grab_region(&mut self, r: Rect) -> Result<Option<Frame>> {
self.n += 1;
let v = if self.n.is_multiple_of(2) { 8 } else { 9 };
Ok(Some(Frame::bgra8(
r.width,
r.height,
vec![v; r.width * r.height * 4],
)))
}
}
let (m, finds, _) = spy();
let mut f = Finder::new(Box::new(TogglingRegion { n: 0 }), Box::new(m));
f.set_region(Some(Rect::new(4, 4, 24, 24)));
for _ in 0..3 {
assert!(f.find_on_screen(&target_tpl()).unwrap().is_some());
}
assert_eq!(finds.get(), 3, "帧内容交替变化,每次都该重搜");
}
#[test]
fn region_fully_offscreen_is_none_not_error() {
let mut f = finder_with(vec![true]);
f.set_region(Some(Rect::new(5000, 5000, 64, 64)));
assert!(
f.find_on_screen(&target_tpl()).unwrap().is_none(),
"越界区域应报告无命中,而不是 panic 或 Err"
);
}
#[test]
fn oversized_timeout_means_wait_forever_not_panic() {
let mut f = finder_with(vec![false, false, true]);
let m = f
.find_until(&target_tpl(), Duration::MAX, Duration::from_millis(2))
.expect("mock 不会出错")
.expect("超时的语义是不限,等到就该返回命中");
assert_eq!((m.x, m.y), (10, 10));
let mut f2 = finder_with(vec![true, false]);
assert!(
f2.wait_gone(&target_tpl(), Duration::MAX, Duration::from_millis(2))
.unwrap(),
"目标消失即返回 true,不应被超大 timeout 炸掉"
);
}
#[test]
fn find_center_returns_center_coords() {
let mut f = finder_with(vec![true]);
let m = f
.find_center_on_screen(&target_tpl())
.unwrap()
.expect("应命中");
assert_eq!((m.x, m.y), (10 + 8 / 2, 10 + 8 / 2));
}
#[test]
fn diff_since_last_first_call_returns_area() {
let mut f = finder_with(vec![true]);
let d = f.diff_since_last(Rect::new(0, 0, 10, 10)).unwrap();
assert_eq!(d, 100);
}
#[test]
fn diff_since_last_same_frame_returns_zero() {
let mut f = finder_with(vec![true]);
let _ = f.diff_since_last(Rect::new(0, 0, 32, 32)).unwrap();
let d = f.diff_since_last(Rect::new(0, 0, 32, 32)).unwrap();
assert_eq!(d, 0);
}
#[test]
fn diff_since_last_detects_change() {
let mut f = finder_with(vec![false, true]);
let _ = f.diff_since_last(Rect::new(0, 0, 32, 32)).unwrap();
let d = f.diff_since_last(Rect::new(0, 0, 32, 32)).unwrap();
assert!(d > 0, "两帧不同应检出变化,got {}", d);
}
struct RegionCapture;
impl Capture for RegionCapture {
fn grab(&mut self) -> Result<Frame> {
Ok(Frame::bgra8(64, 64, vec![9u8; 64 * 64 * 4]))
}
fn grab_region(&mut self, r: Rect) -> Result<Option<Frame>> {
Ok(Some(Frame::bgra8(
r.width,
r.height,
vec![9u8; r.width * r.height * 4],
)))
}
}
#[test]
fn diff_since_last_survives_region_resize() {
let mut f = Finder::new(Box::new(RegionCapture), Box::new(RgbMatcher::new(0)));
f.set_region(Some(Rect::new(0, 0, 8, 8)));
let _ = f.diff_since_last(Rect::new(0, 0, 8, 8)).unwrap();
f.set_region(Some(Rect::new(0, 0, 64, 64)));
let d = f.diff_since_last(Rect::new(0, 0, 16, 16)).unwrap();
assert_eq!(d, 16 * 16, "两帧布局不同应保守报告整片变化");
let d2 = f.diff_since_last(Rect::new(0, 0, 16, 16)).unwrap();
assert_eq!(d2, 0);
}
}