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
78use std:: collections:: HashMap ;
89use std:: sync:: Arc ;
910use std:: time:: { Duration , Instant } ;
1011
1112use botloft_core:: ids:: BotId ;
12- use botloft_core:: protocol:: { BotState , DesktopState } ;
13+ use botloft_core:: protocol:: { BotState , DesktopActionKind , DesktopState } ;
1314use tokio:: sync:: broadcast:: error:: RecvError ;
1415
1516use super :: notice:: color;
16- use crate :: platform:: desktop;
17+ use crate :: platform:: desktop:: { self , ScreenCursor } ;
1718use 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 ) ]
3946struct Follow {
@@ -44,11 +51,12 @@ struct Follow {
4451}
4552
4653impl 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) ]
153187mod 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}
0 commit comments