Skip to content

Commit ce59a3a

Browse files
authored
Merge pull request #187 from httpsphl/feat/desktop-screen-cursor
feat(desktop): the bot's cursor on the owner's screen
2 parents 7b9c801 + 617de75 commit ce59a3a

8 files changed

Lines changed: 409 additions & 58 deletions

File tree

‎crates/botloftd/src/desktop/outline.rs‎

Lines changed: 109 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22
//! 24.9): it shows when a bot reads or acts in a window and follows the
33
//! window while the bot's turn goes on, until the bot leaves the desktop
44
//! alone for a minute, its turn ends or the owner stops it. One window at
5-
//! a time: the one used last.
5+
//! a time: the one used last. The bot's cursor goes with it, at the point
6+
//! of its last action in that window.
67
78
use std::collections::HashMap;
89
use std::sync::Arc;
910
use std::time::{Duration, Instant};
1011

1112
use botloft_core::ids::BotId;
12-
use botloft_core::protocol::{BotState, DesktopState};
13+
use botloft_core::protocol::{BotState, DesktopActionKind, DesktopState};
1314
use tokio::sync::broadcast::error::RecvError;
1415

1516
use super::notice::color;
16-
use crate::platform::desktop;
17+
use crate::platform::desktop::{self, ScreenCursor};
1718
use crate::state::{Daemon, Event};
1819

1920
/// How often the outline catches up with its window.
@@ -29,11 +30,17 @@ struct Outlined {
2930
bot: BotId,
3031
window: u64,
3132
color: (u8, u8, u8),
33+
name: String,
34+
/// Where the bot acted last in the window.
35+
cursor: Option<ScreenCursor>,
3236
used: Instant,
3337
/// When the bot's turn ended, if it did.
3438
turn_over: Option<Instant>,
3539
}
3640

41+
/// What the outline shows now: the window, the bot's color and cursor.
42+
type Shown = (u64, (u8, u8, u8), Option<ScreenCursor>);
43+
3744
/// What the outline does, apart from the screen.
3845
#[derive(Debug, Default)]
3946
struct Follow {
@@ -44,11 +51,12 @@ struct Follow {
4451
}
4552

4653
impl Follow {
47-
/// A bot read or acted in a window, or the owner stopped it.
54+
/// A bot read or acted in a window, or the owner stopped it; `who`
55+
/// gives the bot's color and name.
4856
fn changed(
4957
&mut self,
5058
state: &DesktopState,
51-
color: impl FnOnce() -> (u8, u8, u8),
59+
who: impl FnOnce() -> ((u8, u8, u8), String),
5260
now: Instant,
5361
) {
5462
let mine = self
@@ -68,14 +76,34 @@ impl Follow {
6876
if self.last.insert(state.bot_id.clone(), at) == Some(at) {
6977
return;
7078
}
71-
let color = match &self.outlined {
72-
Some(outlined) if mine => outlined.color,
73-
_ => color(),
79+
let (color, name, before) = match self.outlined.take() {
80+
Some(outlined) if mine => {
81+
let before = outlined.cursor.filter(|_| outlined.window == window.id);
82+
(outlined.color, outlined.name, before)
83+
}
84+
_ => {
85+
let (color, name) = who();
86+
(color, name, None)
87+
}
88+
};
89+
// Only reading leaves the cursor where it was.
90+
let cursor = match &state.action {
91+
Some(action) => action.x.zip(action.y).map(|(x, y)| ScreenCursor {
92+
x,
93+
y,
94+
click: action.kind == DesktopActionKind::Click,
95+
typing: action.kind == DesktopActionKind::Type,
96+
name: name.clone(),
97+
at,
98+
}),
99+
None => before,
74100
};
75101
self.outlined = Some(Outlined {
76102
bot: state.bot_id.clone(),
77103
window: window.id,
78104
color,
105+
name,
106+
cursor,
79107
used: now,
80108
turn_over: None,
81109
});
@@ -93,7 +121,7 @@ impl Follow {
93121
}
94122

95123
/// The window to outline now, if any.
96-
fn now(&mut self, now: Instant) -> Option<(u64, (u8, u8, u8))> {
124+
fn now(&mut self, now: Instant) -> Option<Shown> {
97125
let outlined = self.outlined.as_ref()?;
98126
let over = outlined
99127
.turn_over
@@ -103,7 +131,7 @@ impl Follow {
103131
self.outlined = None;
104132
return None;
105133
}
106-
Some((outlined.window, outlined.color))
134+
Some((outlined.window, outlined.color, outlined.cursor.clone()))
107135
}
108136
}
109137

@@ -121,8 +149,8 @@ pub async fn run(daemon: Arc<Daemon>) {
121149
loop {
122150
tokio::select! {
123151
_ = tick.tick() => match follow.now(Instant::now()) {
124-
Some((window, color)) => {
125-
desktop::outline_show(window, color);
152+
Some((window, color, cursor)) => {
153+
desktop::outline_show(window, color, cursor);
126154
shown = true;
127155
}
128156
None if shown => {
@@ -133,11 +161,17 @@ pub async fn run(daemon: Arc<Daemon>) {
133161
},
134162
event = events.recv() => match event {
135163
Ok(Event::DesktopChanged(state)) => {
136-
let hex = || {
164+
let who = || {
137165
let bot = daemon.store().bot(&state.bot_id).ok().flatten();
138-
color(&bot.map(|bot| bot.color).unwrap_or_default())
166+
let (hex, name) = bot.map(|bot| (bot.color, bot.name)).unwrap_or_default();
167+
(color(&hex), name)
139168
};
140-
follow.changed(&state, hex, Instant::now());
169+
follow.changed(&state, who, Instant::now());
170+
// The cursor sets off now, not at the next tick.
171+
if let Some((window, color, cursor)) = follow.now(Instant::now()) {
172+
desktop::outline_show(window, color, cursor);
173+
shown = true;
174+
}
141175
}
142176
Ok(Event::BotState(change)) => {
143177
follow.turn(&change.bot_id, working(change.state), Instant::now());
@@ -151,7 +185,7 @@ pub async fn run(daemon: Arc<Daemon>) {
151185

152186
#[cfg(test)]
153187
mod tests {
154-
use botloft_core::protocol::DesktopWindow;
188+
use botloft_core::protocol::{DesktopAction, DesktopWindow};
155189

156190
use super::*;
157191

@@ -171,47 +205,90 @@ mod tests {
171205

172206
const BLUE: (u8, u8, u8) = (0x5E, 0xC8, 0xFF);
173207

208+
/// The window and color outlined at `now`, without the cursor.
209+
fn spot(follow: &mut Follow, now: Instant) -> Option<(u64, (u8, u8, u8))> {
210+
follow.now(now).map(|(window, color, _)| (window, color))
211+
}
212+
213+
#[test]
214+
fn the_cursor_is_at_the_last_action_and_stays_while_the_bot_reads() {
215+
let mut follow = Follow::default();
216+
let (scout, start) = (BotId::generate(), Instant::now());
217+
let mut clicked = used(&scout, 7, 1);
218+
clicked.action = Some(DesktopAction {
219+
kind: DesktopActionKind::Click,
220+
target: "Save".to_owned(),
221+
option: None,
222+
x: Some(0.25),
223+
y: Some(0.5),
224+
});
225+
follow.changed(&clicked, || (BLUE, "Scout".to_owned()), start);
226+
let cursor = follow
227+
.now(start)
228+
.and_then(|(_, _, cursor)| cursor)
229+
.expect("cursor");
230+
assert_eq!(
231+
(cursor.x, cursor.y, cursor.click, cursor.typing),
232+
(0.25, 0.5, true, false)
233+
);
234+
assert_eq!((cursor.name.as_str(), cursor.at), ("Scout", 1));
235+
236+
follow.changed(&used(&scout, 7, 2), || (BLUE, "Scout".to_owned()), start);
237+
let kept = follow.now(start).and_then(|(_, _, cursor)| cursor);
238+
assert_eq!(kept.map(|cursor| cursor.at), Some(1), "reading leaves it");
239+
// Another window starts without one.
240+
follow.changed(&used(&scout, 8, 3), || (BLUE, "Scout".to_owned()), start);
241+
assert_eq!(follow.now(start).and_then(|(_, _, cursor)| cursor), None);
242+
}
243+
174244
#[test]
175245
fn the_outline_follows_the_window_in_use_until_the_turn_ends() {
176246
let mut follow = Follow::default();
177247
let (scout, start) = (BotId::generate(), Instant::now());
178-
assert_eq!(follow.now(start), None);
179-
follow.changed(&used(&scout, 7, 1), || BLUE, start);
180-
assert_eq!(follow.now(start), Some((7, BLUE)));
248+
assert_eq!(spot(&mut follow, start), None);
249+
follow.changed(&used(&scout, 7, 1), || (BLUE, "Scout".to_owned()), start);
250+
assert_eq!(spot(&mut follow, start), Some((7, BLUE)));
181251

182252
// Its turn ends: a moment more, then gone.
183253
follow.turn(&scout, false, start);
184-
assert_eq!(follow.now(start + Duration::from_secs(1)), Some((7, BLUE)));
185-
assert_eq!(follow.now(start + AFTER_TURN), None);
254+
assert_eq!(
255+
spot(&mut follow, start + Duration::from_secs(1)),
256+
Some((7, BLUE))
257+
);
258+
assert_eq!(spot(&mut follow, start + AFTER_TURN), None);
186259
}
187260

188261
#[test]
189262
fn a_minute_away_from_the_desktop_or_a_stop_takes_it_away() {
190263
let mut follow = Follow::default();
191264
let (scout, start) = (BotId::generate(), Instant::now());
192-
follow.changed(&used(&scout, 7, 1), || BLUE, start);
193-
assert_eq!(follow.now(start + IDLE_FOR), None);
265+
follow.changed(&used(&scout, 7, 1), || (BLUE, "Scout".to_owned()), start);
266+
assert_eq!(spot(&mut follow, start + IDLE_FOR), None);
194267

195-
follow.changed(&used(&scout, 7, 2), || BLUE, start);
268+
follow.changed(&used(&scout, 7, 2), || (BLUE, "Scout".to_owned()), start);
196269
let mut stopped = used(&scout, 7, 2);
197270
stopped.stopped = true;
198-
follow.changed(&stopped, || BLUE, start);
199-
assert_eq!(follow.now(start), None);
271+
follow.changed(&stopped, || (BLUE, "Scout".to_owned()), start);
272+
assert_eq!(spot(&mut follow, start), None);
200273
// Letting it go on is not a use.
201274
stopped.stopped = false;
202-
follow.changed(&stopped, || BLUE, start);
203-
assert_eq!(follow.now(start), None);
275+
follow.changed(&stopped, || (BLUE, "Scout".to_owned()), start);
276+
assert_eq!(spot(&mut follow, start), None);
204277
}
205278

206279
#[test]
207280
fn the_window_used_last_is_the_one_outlined() {
208281
let mut follow = Follow::default();
209282
let (scout, writer, start) = (BotId::generate(), BotId::generate(), Instant::now());
210-
follow.changed(&used(&scout, 7, 1), || BLUE, start);
211-
follow.changed(&used(&writer, 9, 2), || (1, 2, 3), start);
212-
assert_eq!(follow.now(start), Some((9, (1, 2, 3))));
283+
follow.changed(&used(&scout, 7, 1), || (BLUE, "Scout".to_owned()), start);
284+
follow.changed(
285+
&used(&writer, 9, 2),
286+
|| ((1, 2, 3), "Writer".to_owned()),
287+
start,
288+
);
289+
assert_eq!(spot(&mut follow, start), Some((9, (1, 2, 3))));
213290
// Another bot's turn ending leaves it.
214291
follow.turn(&scout, false, start);
215-
assert_eq!(follow.now(start + AFTER_TURN), Some((9, (1, 2, 3))));
292+
assert_eq!(spot(&mut follow, start + AFTER_TURN), Some((9, (1, 2, 3))));
216293
}
217294
}

‎crates/botloftd/src/platform/desktop/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ mod windows;
1616
use std::path::PathBuf;
1717

1818
pub use blocked::{Never, never};
19-
pub use overlay::{notice_hide, notice_show, outline_hide, outline_show};
19+
pub use overlay::{ScreenCursor, notice_hide, notice_show, outline_hide, outline_show};
2020
pub use read::{Control, LINES_MAX, by_reference, reference, render};
2121

2222
/// The largest picture of a window the bot gets, in logical pixels (24.4).

‎crates/botloftd/src/platform/desktop/overlay.rs‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@
55
#[cfg(windows)]
66
use super::windows;
77

8+
/// The bot's cursor on the owner's screen, over the window it uses.
9+
#[derive(Debug, Clone, PartialEq)]
10+
pub struct ScreenCursor {
11+
/// Where, as fractions of the window's frame from its top left corner.
12+
pub x: f64,
13+
pub y: f64,
14+
/// A ring shows where it clicked.
15+
pub click: bool,
16+
/// Dots show while it types.
17+
pub typing: bool,
18+
/// The bot's name, beside the arrow.
19+
pub name: String,
20+
/// When the bot acted: a new one glides there.
21+
pub at: i64,
22+
}
23+
824
/// Shows the notice that a bot uses the real mouse and keyboard: `text`
925
/// in a pill, a border in `color` around the monitor of window `over`
1026
/// (spec 24.7).
@@ -21,13 +37,14 @@ pub fn notice_hide() {
2137
windows::notice_hide();
2238
}
2339

24-
/// Outlines window `over` in `color`, or moves the outline to where the
25-
/// window is now; hidden while the window is minimized or gone.
26-
pub fn outline_show(over: u64, color: (u8, u8, u8)) {
40+
/// Outlines window `over` in `color`, with the bot's `cursor` in it, or
41+
/// moves the outline to where the window is now; hidden while the window
42+
/// is minimized or gone.
43+
pub fn outline_show(over: u64, color: (u8, u8, u8), cursor: Option<ScreenCursor>) {
2744
#[cfg(windows)]
28-
windows::outline_show(over, color);
45+
windows::outline_show(over, color, cursor);
2946
#[cfg(not(windows))]
30-
let _ = (over, color);
47+
let _ = (over, color, cursor);
3148
}
3249

3350
/// Takes the outline away.

‎crates/botloftd/src/platform/desktop/windows/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod capture;
99
mod input;
1010
mod notice;
1111
mod outline;
12+
mod outline_cursor;
1213
#[cfg(test)]
1314
mod outline_tests;
1415
mod owner;

0 commit comments

Comments
 (0)