@desklink/host 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +3 -0
- package/README.md +63 -15
- package/bin/desklink-host.mjs +12 -4
- package/dist/engineProcess.d.ts +6 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +34 -4
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +32 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +6 -0
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +45 -6
- package/dist/resolveEngine.js.map +1 -1
- package/docs/PROTOCOL.md +119 -6
- package/engine/Cargo.lock +2 -2
- package/engine/Cargo.toml +7 -12
- package/engine/build.rs +61 -5
- package/engine/native/agent_overlay_mac.m +51 -4
- package/engine/native/agent_overlay_wayland.c +145 -12
- package/engine/native/agent_overlay_x11.c +72 -0
- package/engine/src/capture.rs +92 -3
- package/engine/src/capture_win.rs +23 -0
- package/engine/src/clipboard_win.rs +8 -0
- package/engine/src/cursor.rs +118 -0
- package/engine/src/indicator.rs +104 -3
- package/engine/src/input_win.rs +91 -0
- package/engine/src/keymap_win.rs +39 -0
- package/engine/src/mac.rs +28 -6
- package/engine/src/main.rs +165 -23
- package/engine/src/portal.rs +42 -4
- package/engine/src/portal_win.rs +37 -0
- package/engine/src/protocol.rs +49 -0
- package/engine/src/session.rs +467 -17
- package/engine/src/win.rs +110 -0
- package/engine/src/x11.rs +15 -0
- package/package.json +4 -4
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
#include <stdio.h>
|
|
18
18
|
#include <stdlib.h>
|
|
19
19
|
#include <string.h>
|
|
20
|
+
#include <fontconfig/fontconfig.h>
|
|
21
|
+
#include <ft2build.h>
|
|
22
|
+
#include FT_FREETYPE_H
|
|
20
23
|
|
|
21
24
|
// Layer-shell v1 protocol (Drew DeVault, 2017; MIT license).
|
|
22
25
|
static const struct wl_interface layer_surface_interface;
|
|
@@ -35,6 +38,38 @@ static const struct wl_message surface_requests[] = {
|
|
|
35
38
|
static const struct wl_message surface_events[] = { {"configure", "uuu", NULL}, {"closed", "", NULL} };
|
|
36
39
|
static const struct wl_interface layer_surface_interface = {"zwlr_layer_surface_v1", 1, 8, surface_requests, 2, surface_events};
|
|
37
40
|
|
|
41
|
+
/* xdg-output wire signatures: Copyright (c) 2017 Red Hat Inc.
|
|
42
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
43
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
44
|
+
* in the Software without restriction, including without limitation the rights
|
|
45
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
46
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
47
|
+
* furnished to do so, subject to the following conditions:
|
|
48
|
+
* The above copyright notice and this permission notice shall be included in
|
|
49
|
+
* all copies or substantial portions of the Software.
|
|
50
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
51
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
52
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
53
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
54
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
55
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
56
|
+
* SOFTWARE.
|
|
57
|
+
*/
|
|
58
|
+
// xdg-output v2 supplies compositor logical coordinates, including scaled outputs.
|
|
59
|
+
static const struct wl_interface xdg_output_interface;
|
|
60
|
+
static const struct wl_interface *xdg_types[]={&xdg_output_interface,&wl_output_interface};
|
|
61
|
+
static const struct wl_message xdg_requests[]={{"destroy","",NULL},{"get_xdg_output","no",xdg_types}};
|
|
62
|
+
static const struct wl_interface xdg_manager_interface={"zxdg_output_manager_v1",2,2,xdg_requests,0,NULL};
|
|
63
|
+
static const struct wl_message output_requests[]={{"destroy","",NULL}};
|
|
64
|
+
static const struct wl_message output_events[]={{"logical_position","ii",NULL},{"logical_size","ii",NULL},{"done","",NULL},{"name","2s",NULL},{"description","2s",NULL}};
|
|
65
|
+
static const struct wl_interface xdg_output_interface={"zxdg_output_v1",2,1,output_requests,5,output_events};
|
|
66
|
+
struct Output { struct wl_output *handle; struct wl_proxy *logical; int x,y,w,h,valid; };
|
|
67
|
+
static void logical_position(void *data, struct wl_proxy *p,int32_t x,int32_t y){(void)p;struct Output *o=data;o->x=x;o->y=y;o->valid=1;}
|
|
68
|
+
static void logical_size(void *data,struct wl_proxy *p,int32_t w,int32_t h){(void)p;struct Output *o=data;o->w=w;o->h=h;}
|
|
69
|
+
static void logical_done(void *data,struct wl_proxy *p){(void)data;(void)p;}
|
|
70
|
+
static void logical_string(void *data,struct wl_proxy *p,const char *s){(void)data;(void)p;(void)s;}
|
|
71
|
+
static void (*logical_listener[])(void)={(void (*)(void))logical_position,(void (*)(void))logical_size,(void (*)(void))logical_done,(void (*)(void))logical_string,(void (*)(void))logical_string};
|
|
72
|
+
|
|
38
73
|
int desklink_wayland_geometry(int source_w, int source_h, int surface_w, int surface_h,
|
|
39
74
|
double px, double py, double *x, double *y) {
|
|
40
75
|
if (source_w<=0||source_h<=0||surface_w<=0||surface_h<=0) return 0;
|
|
@@ -50,7 +85,8 @@ struct Overlay {
|
|
|
50
85
|
struct wl_display *display;
|
|
51
86
|
struct wl_compositor *compositor;
|
|
52
87
|
struct wl_shm *shm;
|
|
53
|
-
struct wl_proxy *shell, *layer;
|
|
88
|
+
struct wl_proxy *shell, *layer, *output_manager;
|
|
89
|
+
struct Output outputs[32]; int output_count;
|
|
54
90
|
struct wl_surface *surface;
|
|
55
91
|
struct Buffer buffers[2];
|
|
56
92
|
int width, height, source_w, source_h, configured, closed;
|
|
@@ -58,6 +94,11 @@ struct Overlay {
|
|
|
58
94
|
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec+t.tv_nsec/1e9; }
|
|
59
95
|
static void registry_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
|
|
60
96
|
struct Overlay *o=data;
|
|
97
|
+
if (!strcmp(interface,"wl_output") && o->output_count<32) {
|
|
98
|
+
struct Output *out=&o->outputs[o->output_count++];
|
|
99
|
+
out->handle=wl_registry_bind(registry,name,&wl_output_interface,1);
|
|
100
|
+
}
|
|
101
|
+
if (!strcmp(interface,"zxdg_output_manager_v1")) o->output_manager=wl_registry_bind(registry,name,&xdg_manager_interface,version<2?version:2);
|
|
61
102
|
if (!strcmp(interface,"wl_compositor")) o->compositor=wl_registry_bind(registry,name,&wl_compositor_interface,version<4?version:4);
|
|
62
103
|
if (!strcmp(interface,"wl_shm")) o->shm=wl_registry_bind(registry,name,&wl_shm_interface,1);
|
|
63
104
|
if (!strcmp(interface,"zwlr_layer_shell_v1")) o->shell=wl_registry_bind(registry,name,&shell_interface,1);
|
|
@@ -65,7 +106,11 @@ static void registry_global(void *data, struct wl_registry *registry, uint32_t n
|
|
|
65
106
|
static void registry_remove(void *data, struct wl_registry *registry, uint32_t name) { (void)data;(void)registry;(void)name; }
|
|
66
107
|
static const struct wl_registry_listener registry_listener={registry_global,registry_remove};
|
|
67
108
|
static void configured(void *data, struct wl_proxy *layer, uint32_t serial, uint32_t w, uint32_t h) {
|
|
68
|
-
struct Overlay *o=data;
|
|
109
|
+
struct Overlay *o=data;
|
|
110
|
+
// A source/layout change invalidates this helper; never write new dimensions
|
|
111
|
+
// into buffers allocated for the old output size.
|
|
112
|
+
if(o->configured && (o->width!=(int)w || o->height!=(int)h))o->closed=1;
|
|
113
|
+
o->width=(int)w; o->height=(int)h; o->configured=1;
|
|
69
114
|
wl_proxy_marshal_flags(layer,6,NULL,1,0,serial);
|
|
70
115
|
}
|
|
71
116
|
static void closed(void *data, struct wl_proxy *layer) { (void)layer; ((struct Overlay *)data)->closed=1; }
|
|
@@ -117,16 +162,76 @@ static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double
|
|
|
117
162
|
}
|
|
118
163
|
}
|
|
119
164
|
|
|
120
|
-
|
|
165
|
+
static FT_Face point_font(FT_Library *library) {
|
|
166
|
+
if(FT_Init_FreeType(library))return NULL;
|
|
167
|
+
FcPattern *pattern=FcNameParse((const FcChar8 *)"monospace");
|
|
168
|
+
if(!pattern)return NULL;
|
|
169
|
+
FcConfigSubstitute(NULL,pattern,FcMatchPattern);FcDefaultSubstitute(pattern);
|
|
170
|
+
FcResult result;FcPattern *match=FcFontMatch(NULL,pattern,&result);FcPatternDestroy(pattern);
|
|
171
|
+
FcChar8 *path=NULL;FT_Face face=NULL;
|
|
172
|
+
if(match && FcPatternGetString(match,FC_FILE,0,&path)==FcResultMatch)
|
|
173
|
+
if(!FT_New_Face(*library,(const char *)path,0,&face))FT_Set_Pixel_Sizes(face,0,13);
|
|
174
|
+
if(match)FcPatternDestroy(match);
|
|
175
|
+
return face;
|
|
176
|
+
}
|
|
177
|
+
static void draw_point(struct Overlay *o,struct Buffer *b,double x,double y,const char *label,FT_Face font,int visible) {
|
|
178
|
+
memset(b->pixels,0,b->length);if(!visible)return;
|
|
179
|
+
int w=o->width,h=o->height;
|
|
180
|
+
for(int py=fmax(0,y-21);py<fmin(h,y+22);py++)for(int px=fmax(0,x-21);px<fmin(w,x+22);px++) {
|
|
181
|
+
double d=hypot(px-x,py-y);
|
|
182
|
+
if(fabs(d-18)<=2 || d<=3)b->pixels[(size_t)py*w+px]=0xff4c9ed0;
|
|
183
|
+
}
|
|
184
|
+
if(!*label)return;
|
|
185
|
+
int badge_w=16;
|
|
186
|
+
for(const char *p=label;*p;p++)if(!FT_Load_Char(font,(unsigned char)*p,FT_LOAD_DEFAULT))badge_w+=font->glyph->advance.x>>6;
|
|
187
|
+
badge_w=fmin(w,badge_w);int bx=fmax(0,fmin(x+30,w-badge_w)),by=fmax(0,fmin(y-14,h-28));
|
|
188
|
+
for(int py=by;py<fmin(h,by+28);py++)for(int px=bx;px<bx+badge_w;px++)b->pixels[(size_t)py*w+px]=0xff123047;
|
|
189
|
+
int pen=bx+8;
|
|
190
|
+
for(const char *p=label;*p;p++)if(!FT_Load_Char(font,(unsigned char)*p,FT_LOAD_RENDER)) {
|
|
191
|
+
FT_GlyphSlot g=font->glyph;
|
|
192
|
+
for(unsigned row=0;row<g->bitmap.rows;row++)for(unsigned col=0;col<g->bitmap.width;col++) {
|
|
193
|
+
int px=pen+g->bitmap_left+col,py=by+18-g->bitmap_top+row;
|
|
194
|
+
if(px<0||px>=w||py<0||py>=h)continue;
|
|
195
|
+
unsigned a=g->bitmap.buffer[row*g->bitmap.pitch+col];
|
|
196
|
+
unsigned r=(18*(255-a)+255*a)/255,green=(48*(255-a)+255*a)/255,blue=(71*(255-a)+255*a)/255;
|
|
197
|
+
b->pixels[(size_t)py*w+px]=0xff000000|(r<<16)|(green<<8)|blue;
|
|
198
|
+
}
|
|
199
|
+
pen+=g->advance.x>>6;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static int overlay_main(int source_w,int source_h,int point_mode,int origin_known,int origin_x,int origin_y) {
|
|
204
|
+
|
|
121
205
|
struct Overlay o={.source_w=source_w,.source_h=source_h};
|
|
122
206
|
o.display=wl_display_connect(NULL);
|
|
123
207
|
if(!o.display)return 2;
|
|
124
208
|
struct wl_registry *registry=wl_display_get_registry(o.display);
|
|
125
209
|
wl_registry_add_listener(registry,®istry_listener,&o);
|
|
126
210
|
wl_display_roundtrip(o.display);
|
|
127
|
-
if(!o.
|
|
211
|
+
if(!o.shell){puts("layer_shell_unavailable");fflush(stdout);wl_display_disconnect(o.display);return 2;}
|
|
212
|
+
if(!o.compositor||!o.shm){wl_display_disconnect(o.display);return 2;}
|
|
213
|
+
struct wl_output *selected=NULL;int matches=0;
|
|
214
|
+
if(point_mode) {
|
|
215
|
+
if(o.output_manager) {
|
|
216
|
+
for(int i=0;i<o.output_count;i++) {
|
|
217
|
+
struct Output *out=&o.outputs[i];
|
|
218
|
+
out->logical=wl_proxy_marshal_flags(o.output_manager,1,&xdg_output_interface,wl_proxy_get_version(o.output_manager),0,NULL,out->handle);
|
|
219
|
+
wl_proxy_add_listener(out->logical,logical_listener,out);
|
|
220
|
+
}
|
|
221
|
+
if(wl_display_roundtrip(o.display)<0)return 2;
|
|
222
|
+
}
|
|
223
|
+
for(int i=0;i<o.output_count;i++) {
|
|
224
|
+
struct Output *out=&o.outputs[i];
|
|
225
|
+
if(origin_known && out->valid && origin_x>=out->x && origin_y>=out->y && origin_x<out->x+out->w && origin_y<out->y+out->h){selected=out->handle;matches++;}
|
|
226
|
+
}
|
|
227
|
+
if(matches>1)selected=NULL;
|
|
228
|
+
if(!selected && o.output_count==1 && (!origin_known || !o.output_manager))selected=o.outputs[0].handle;
|
|
229
|
+
if(!selected){puts("selected_output_unavailable");fflush(stdout);wl_display_disconnect(o.display);return 2;}
|
|
230
|
+
}
|
|
231
|
+
FT_Library library=NULL;FT_Face font=point_mode?point_font(&library):NULL;
|
|
232
|
+
if(point_mode&&!font){wl_display_disconnect(o.display);return 2;}
|
|
128
233
|
o.surface=wl_compositor_create_surface(o.compositor);
|
|
129
|
-
o.layer=wl_proxy_marshal_flags(o.shell,0,&layer_surface_interface,1,0,NULL,o.surface,
|
|
234
|
+
o.layer=wl_proxy_marshal_flags(o.shell,0,&layer_surface_interface,1,0,NULL,o.surface,selected,3,point_mode?"desklink-point":"desklink-agent");
|
|
130
235
|
wl_proxy_add_listener(o.layer,layer_listener,&o);
|
|
131
236
|
uint32_t anchor,keyboard_focus,empty_input;
|
|
132
237
|
desklink_wayland_policy(&anchor,&keyboard_focus,&empty_input);
|
|
@@ -147,7 +252,8 @@ int desklink_agent_overlay_wayland(int source_w, int source_h) {
|
|
|
147
252
|
puts("READY");fflush(stdout);
|
|
148
253
|
double tx=0,ty=0,x=0,y=0,click=0,typed=0,ending=0;
|
|
149
254
|
int active=0,positioned=0;
|
|
150
|
-
char
|
|
255
|
+
double deadline=0;char label[97]={0};int dirty=0,visible=0;
|
|
256
|
+
char pending[512];size_t used=0;
|
|
151
257
|
while(!o.closed && (!ending||now()-ending<0.6)) {
|
|
152
258
|
struct pollfd fds[2]={{wl_display_get_fd(o.display),POLLIN,0},{STDIN_FILENO,POLLIN|POLLHUP,0}};
|
|
153
259
|
wl_display_flush(o.display);
|
|
@@ -155,28 +261,39 @@ int desklink_agent_overlay_wayland(int source_w, int source_h) {
|
|
|
155
261
|
if(fds[0].revents&POLLIN && wl_display_dispatch(o.display)<0)break;
|
|
156
262
|
else wl_display_dispatch_pending(o.display);
|
|
157
263
|
char bytes[128];ssize_t n=read(STDIN_FILENO,bytes,sizeof(bytes));
|
|
158
|
-
if(n==0
|
|
264
|
+
if(n==0){if(point_mode)break;if(!ending)ending=now();}
|
|
159
265
|
for(ssize_t i=0;i<n;i++) {
|
|
160
266
|
if(bytes[i]=='\n') {
|
|
161
267
|
pending[used]=0;char kind=0;double px=0,py=0;
|
|
162
268
|
if(sscanf(pending,"%c %lf %lf",&kind,&px,&py)>=1) {
|
|
163
|
-
if(kind=='
|
|
269
|
+
if(point_mode && kind=='P') {
|
|
270
|
+
unsigned timeout=0;int offset=0;
|
|
271
|
+
if(sscanf(pending,"P %lf %lf %u %n",&px,&py,&timeout,&offset)==3 && offset && timeout>=1 && timeout<=120000 && strlen(pending+offset)<=96) {
|
|
272
|
+
desklink_wayland_geometry(o.source_w,o.source_h,o.width,o.height,px,py,&x,&y);
|
|
273
|
+
snprintf(label,sizeof(label),"%s",pending+offset);deadline=now()+timeout/1000.0;active=1;dirty=1;visible=1;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if(kind=='S')ending=now();
|
|
164
277
|
else if(kind=='M'||kind=='C'||kind=='T'||kind=='A') {
|
|
165
278
|
if(px>=0&&py>=0) {
|
|
166
279
|
if(desklink_wayland_geometry(o.source_w,o.source_h,o.width,o.height,px,py,&tx,&ty)
|
|
167
280
|
&& !positioned){x=tx;y=ty;positioned=1;}
|
|
168
281
|
}
|
|
169
282
|
active=1;
|
|
170
|
-
if(kind=='C')click=now();
|
|
283
|
+
if(kind=='C')click=now();
|
|
284
|
+
if(kind=='T')typed=now();
|
|
171
285
|
}
|
|
172
286
|
}
|
|
173
287
|
used=0;
|
|
174
288
|
}else if(used<sizeof(pending)-1)pending[used++]=bytes[i];
|
|
175
289
|
}
|
|
176
|
-
if(
|
|
177
|
-
|
|
290
|
+
if(point_mode && visible && now()>=deadline){visible=0;dirty=1;}
|
|
291
|
+
if(!active || (point_mode&&!dirty))continue;
|
|
292
|
+
if(!point_mode){x+=(tx-x)*0.24;y+=(ty-y)*0.24;}
|
|
178
293
|
for(int i=0;i<2;i++)if(!o.buffers[i].busy) {
|
|
179
|
-
|
|
294
|
+
if(point_mode)draw_point(&o,&o.buffers[i],x,y,label,font,visible);
|
|
295
|
+
else draw(&o,&o.buffers[i],x,y,click,typed,ending,positioned);
|
|
296
|
+
dirty=0;
|
|
180
297
|
o.buffers[i].busy=1;
|
|
181
298
|
wl_surface_attach(o.surface,o.buffers[i].handle,0,0);
|
|
182
299
|
wl_surface_damage_buffer(o.surface,0,0,o.width,o.height);
|
|
@@ -187,6 +304,22 @@ int desklink_agent_overlay_wayland(int source_w, int source_h) {
|
|
|
187
304
|
wl_surface_attach(o.surface,NULL,0,0);wl_surface_commit(o.surface);wl_display_roundtrip(o.display);
|
|
188
305
|
for(int i=0;i<2;i++){wl_buffer_destroy(o.buffers[i].handle);munmap(o.buffers[i].pixels,o.buffers[i].length);}
|
|
189
306
|
wl_proxy_destroy(o.layer);wl_surface_destroy(o.surface);wl_proxy_destroy(o.shell);
|
|
307
|
+
if(font)FT_Done_Face(font);
|
|
308
|
+
if(library)FT_Done_FreeType(library);
|
|
190
309
|
wl_display_disconnect(o.display);
|
|
191
310
|
return 0;
|
|
192
311
|
}
|
|
312
|
+
|
|
313
|
+
int desklink_agent_overlay_wayland(int w,int h){return overlay_main(w,h,0,0,0,0);}
|
|
314
|
+
int desklink_point_overlay_wayland(int w,int h,int known,int x,int y){return overlay_main(w,h,1,known,x,y);}
|
|
315
|
+
|
|
316
|
+
// Read-only capability probe for idempotent clear before a helper exists.
|
|
317
|
+
int desklink_wayland_layer_shell_available(void) {
|
|
318
|
+
struct Overlay o={0};o.display=wl_display_connect(NULL);
|
|
319
|
+
if(!o.display)return -1;
|
|
320
|
+
struct wl_registry *registry=wl_display_get_registry(o.display);
|
|
321
|
+
wl_registry_add_listener(registry,®istry_listener,&o);
|
|
322
|
+
int ok=wl_display_roundtrip(o.display)>=0;
|
|
323
|
+
int result=ok?(o.shell!=NULL):-1;
|
|
324
|
+
wl_display_disconnect(o.display);return result;
|
|
325
|
+
}
|
|
@@ -101,3 +101,75 @@ int desklink_agent_overlay_main(const char *display_name) {
|
|
|
101
101
|
XcursorImageDestroy(image); XCloseDisplay(d);
|
|
102
102
|
return 0;
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
// Point cues are independent of the real cursor. A shaped override-redirect
|
|
106
|
+
// window works without a compositor; an empty input shape passes every event
|
|
107
|
+
// through and mapping it never asks for keyboard focus. It intentionally appears
|
|
108
|
+
// in GetImage(root), so the local person and the video observer share the cue.
|
|
109
|
+
#include <X11/extensions/shape.h>
|
|
110
|
+
#include <stdlib.h>
|
|
111
|
+
|
|
112
|
+
int desklink_point_overlay_main(const char *display_name) {
|
|
113
|
+
Display *d = XOpenDisplay(display_name);
|
|
114
|
+
if (!d) return 2;
|
|
115
|
+
int event_base, error_base;
|
|
116
|
+
if (!XShapeQueryExtension(d, &event_base, &error_base)) { XCloseDisplay(d); return 2; }
|
|
117
|
+
int screen = DefaultScreen(d), width = DisplayWidth(d, screen), height = DisplayHeight(d, screen);
|
|
118
|
+
Window root = RootWindow(d, screen);
|
|
119
|
+
XSetWindowAttributes attrs = {0};
|
|
120
|
+
attrs.override_redirect = True;
|
|
121
|
+
attrs.background_pixel = 0x4c9ed0;
|
|
122
|
+
Window window = XCreateWindow(d, root, 0, 0, width, height, 0,
|
|
123
|
+
CopyFromParent, InputOutput, CopyFromParent, CWOverrideRedirect | CWBackPixel, &attrs);
|
|
124
|
+
XStoreName(d, window, "Desklink point");
|
|
125
|
+
XShapeCombineRectangles(d, window, ShapeInput, 0, 0, NULL, 0, ShapeSet, Unsorted);
|
|
126
|
+
Pixmap mask = XCreatePixmap(d, root, width, height, 1);
|
|
127
|
+
GC shape = XCreateGC(d, mask, 0, NULL), ink = XCreateGC(d, window, 0, NULL);
|
|
128
|
+
XFontStruct *font = XLoadQueryFont(d, "fixed");
|
|
129
|
+
if (font) XSetFont(d, ink, font->fid);
|
|
130
|
+
XSetLineAttributes(d, shape, 4, LineSolid, CapRound, JoinRound);
|
|
131
|
+
fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK);
|
|
132
|
+
puts("READY"); fflush(stdout);
|
|
133
|
+
char pending[512]; size_t used = 0;
|
|
134
|
+
double deadline = 0;
|
|
135
|
+
for (;;) {
|
|
136
|
+
char bytes[512]; ssize_t n = read(STDIN_FILENO, bytes, sizeof(bytes));
|
|
137
|
+
if (n == 0) break;
|
|
138
|
+
for (ssize_t i = 0; i < n; i++) {
|
|
139
|
+
if (bytes[i] != '\n') { if (used < sizeof(pending)-1) pending[used++] = bytes[i]; continue; }
|
|
140
|
+
pending[used] = 0; used = 0;
|
|
141
|
+
double px, py; unsigned timeout; int offset = 0;
|
|
142
|
+
if (sscanf(pending, "P %lf %lf %u %n", &px, &py, &timeout, &offset) != 3 || !offset) continue;
|
|
143
|
+
const char *label = pending + offset;
|
|
144
|
+
int x = (int)px, y = (int)py, len = (int)strlen(label);
|
|
145
|
+
int badge_w = font ? XTextWidth(font, label, len) + 16 : len * 6 + 16;
|
|
146
|
+
if (badge_w > width) badge_w = width;
|
|
147
|
+
int badge_x = x + 30, badge_y = y - 14;
|
|
148
|
+
if (badge_x + badge_w > width) badge_x = width - badge_w;
|
|
149
|
+
if (badge_y < 0) badge_y = 0;
|
|
150
|
+
if (badge_y + 28 > height) badge_y = height - 28;
|
|
151
|
+
XUnmapWindow(d, window);
|
|
152
|
+
XSetForeground(d, shape, 0); XFillRectangle(d, mask, shape, 0, 0, width, height);
|
|
153
|
+
XSetForeground(d, shape, 1);
|
|
154
|
+
XDrawArc(d, mask, shape, x-18, y-18, 36, 36, 0, 360*64);
|
|
155
|
+
XFillArc(d, mask, shape, x-3, y-3, 6, 6, 0, 360*64);
|
|
156
|
+
if (len) XFillRectangle(d, mask, shape, badge_x, badge_y, badge_w, 28);
|
|
157
|
+
XShapeCombineMask(d, window, ShapeBounding, 0, 0, mask, ShapeSet);
|
|
158
|
+
XMapRaised(d, window);
|
|
159
|
+
XClearWindow(d, window);
|
|
160
|
+
if (len) {
|
|
161
|
+
XSetForeground(d, ink, 0x123047);
|
|
162
|
+
XFillRectangle(d, window, ink, badge_x, badge_y, badge_w, 28);
|
|
163
|
+
XSetForeground(d, ink, 0xffffff);
|
|
164
|
+
XDrawString(d, window, ink, badge_x+8, badge_y+18, label, len);
|
|
165
|
+
}
|
|
166
|
+
XSync(d, False);
|
|
167
|
+
deadline = now() + timeout/1000.0;
|
|
168
|
+
}
|
|
169
|
+
if (deadline && now() >= deadline) { XUnmapWindow(d, window); XFlush(d); deadline = 0; }
|
|
170
|
+
usleep(5000);
|
|
171
|
+
}
|
|
172
|
+
XDestroyWindow(d, window); XFreeGC(d, shape); XFreeGC(d, ink);
|
|
173
|
+
XFreePixmap(d, mask); if (font) XFreeFont(d, font); XCloseDisplay(d);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
package/engine/src/capture.rs
CHANGED
|
@@ -13,7 +13,7 @@ use pw::spa;
|
|
|
13
13
|
use pw::spa::pod::{serialize::PodSerializer, Pod};
|
|
14
14
|
use std::io::Cursor;
|
|
15
15
|
use std::os::fd::OwnedFd;
|
|
16
|
-
use std::sync::atomic::{AtomicU64, Ordering};
|
|
16
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
17
17
|
use std::sync::{mpsc, Arc, Mutex};
|
|
18
18
|
use std::thread::JoinHandle;
|
|
19
19
|
use std::time::{Duration, Instant};
|
|
@@ -55,6 +55,7 @@ pub struct Capture {
|
|
|
55
55
|
quit: pw::channel::Sender<()>,
|
|
56
56
|
thread: Option<JoinHandle<()>>,
|
|
57
57
|
pub source: SelectedSource,
|
|
58
|
+
cursor_positions: Arc<AtomicBool>,
|
|
58
59
|
geometry: Arc<Mutex<Option<StreamGeometry>>>,
|
|
59
60
|
frames: Arc<AtomicU64>,
|
|
60
61
|
dropped: Arc<AtomicU64>,
|
|
@@ -63,6 +64,9 @@ pub struct Capture {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
impl Capture {
|
|
67
|
+
pub fn cursor_positions(&self) -> bool {
|
|
68
|
+
self.cursor_positions.load(Ordering::Relaxed)
|
|
69
|
+
}
|
|
66
70
|
pub fn geometry(&self) -> Option<StreamGeometry> {
|
|
67
71
|
self.geometry.lock().ok().and_then(|g| g.clone())
|
|
68
72
|
}
|
|
@@ -193,16 +197,29 @@ fn buffer_pod(buffer: &mut Vec<u8>) -> Result<()> {
|
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
/// Start capturing `session`'s node, delivering I420 frames to `sink`.
|
|
200
|
+
#[allow(clippy::too_many_arguments)]
|
|
196
201
|
pub fn start(
|
|
197
202
|
session: PortalSession,
|
|
198
203
|
encoded_width: usize,
|
|
199
204
|
encoded_height: usize,
|
|
200
205
|
max_fps: u32,
|
|
206
|
+
cursor_sink: Option<crate::cursor::CursorSink>,
|
|
201
207
|
sink: FrameSink,
|
|
202
208
|
indicator_position: Option<Arc<Mutex<Option<(i64, i64)>>>>,
|
|
203
209
|
on_stop: Box<dyn Fn(String) + Send>,
|
|
204
210
|
) -> Result<Capture> {
|
|
205
|
-
let PortalSession {
|
|
211
|
+
let PortalSession {
|
|
212
|
+
fd,
|
|
213
|
+
source,
|
|
214
|
+
cursor_mode,
|
|
215
|
+
..
|
|
216
|
+
} = session;
|
|
217
|
+
let cursor_positions = Arc::new(AtomicBool::new(false));
|
|
218
|
+
let cursor_sink = if cursor_mode == "metadata" {
|
|
219
|
+
cursor_sink
|
|
220
|
+
} else {
|
|
221
|
+
None
|
|
222
|
+
};
|
|
206
223
|
let geometry = Arc::new(Mutex::new(None));
|
|
207
224
|
let frames = Arc::new(AtomicU64::new(0));
|
|
208
225
|
let dropped = Arc::new(AtomicU64::new(0));
|
|
@@ -210,7 +227,9 @@ pub fn start(
|
|
|
210
227
|
let (ready_tx, ready_rx) = mpsc::channel::<Result<(), String>>();
|
|
211
228
|
|
|
212
229
|
let node_id = source.node_id;
|
|
230
|
+
let source_size = (source.width.max(1) as usize, source.height.max(1) as usize);
|
|
213
231
|
let thread = {
|
|
232
|
+
let cursor_positions = cursor_positions.clone();
|
|
214
233
|
let geometry = geometry.clone();
|
|
215
234
|
let frames = frames.clone();
|
|
216
235
|
let dropped = dropped.clone();
|
|
@@ -221,9 +240,12 @@ pub fn start(
|
|
|
221
240
|
let result = run_loop(
|
|
222
241
|
fd,
|
|
223
242
|
node_id,
|
|
243
|
+
source_size,
|
|
224
244
|
encoded_width,
|
|
225
245
|
encoded_height,
|
|
226
246
|
max_fps,
|
|
247
|
+
cursor_sink,
|
|
248
|
+
cursor_positions,
|
|
227
249
|
sink,
|
|
228
250
|
indicator_position,
|
|
229
251
|
on_stop,
|
|
@@ -250,6 +272,7 @@ pub fn start(
|
|
|
250
272
|
quit,
|
|
251
273
|
thread: Some(thread),
|
|
252
274
|
source,
|
|
275
|
+
cursor_positions,
|
|
253
276
|
geometry,
|
|
254
277
|
frames,
|
|
255
278
|
dropped,
|
|
@@ -268,9 +291,12 @@ pub fn start(
|
|
|
268
291
|
fn run_loop(
|
|
269
292
|
fd: OwnedFd,
|
|
270
293
|
node_id: u32,
|
|
294
|
+
source_size: (usize, usize),
|
|
271
295
|
encoded_width: usize,
|
|
272
296
|
encoded_height: usize,
|
|
273
297
|
max_fps: u32,
|
|
298
|
+
cursor_sink: Option<crate::cursor::CursorSink>,
|
|
299
|
+
cursor_positions: Arc<AtomicBool>,
|
|
274
300
|
sink: FrameSink,
|
|
275
301
|
indicator_position: Option<Arc<Mutex<Option<(i64, i64)>>>>,
|
|
276
302
|
on_stop: Box<dyn Fn(String) + Send>,
|
|
@@ -302,6 +328,8 @@ fn run_loop(
|
|
|
302
328
|
let stream = pw::stream::StreamBox::new(&core, "desklink-capture", props)
|
|
303
329
|
.context("pw_stream_new failed")?;
|
|
304
330
|
|
|
331
|
+
let wants_cursor_meta = cursor_sink.is_some();
|
|
332
|
+
let mut cursor = cursor_sink.map(crate::cursor::Reporter::new);
|
|
305
333
|
let mut state = StreamState {
|
|
306
334
|
box_w: encoded_width,
|
|
307
335
|
box_h: encoded_height,
|
|
@@ -367,6 +395,31 @@ fn run_loop(
|
|
|
367
395
|
let Some(mut buffer) = stream.dequeue_buffer() else {
|
|
368
396
|
return;
|
|
369
397
|
};
|
|
398
|
+
if let Some(cursor) = cursor.as_mut() {
|
|
399
|
+
if let Some(meta) = buffer.find_meta::<spa::buffer::meta::MetaCursor>() {
|
|
400
|
+
cursor_positions.store(true, Ordering::Relaxed);
|
|
401
|
+
// The wrapper's size check bounds the bitmap header;
|
|
402
|
+
// never follow an unchecked bitmap offset.
|
|
403
|
+
let bitmap = buffer
|
|
404
|
+
.find_meta::<CursorWithBitmap>()
|
|
405
|
+
.filter(|m| {
|
|
406
|
+
m.cursor.bitmap_offset as usize
|
|
407
|
+
== std::mem::size_of::<spa::sys::spa_meta_cursor>()
|
|
408
|
+
})
|
|
409
|
+
.map(|m| &m.bitmap);
|
|
410
|
+
if state.logical_w == 0 || state.logical_h == 0 {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
let mut position = *meta.as_raw();
|
|
414
|
+
position.position.x = (i64::from(position.position.x) * source_size.0 as i64)
|
|
415
|
+
.div_euclid(state.logical_w as i64)
|
|
416
|
+
as i32;
|
|
417
|
+
position.position.y = (i64::from(position.position.y) * source_size.1 as i64)
|
|
418
|
+
.div_euclid(state.logical_h as i64)
|
|
419
|
+
as i32;
|
|
420
|
+
cursor.update_spa(&position, bitmap, source_size.0, source_size.1);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
370
423
|
let Some(format) = state.format else { return };
|
|
371
424
|
let datas = buffer.datas_mut();
|
|
372
425
|
if datas.is_empty() {
|
|
@@ -463,10 +516,15 @@ fn run_loop(
|
|
|
463
516
|
format_pod(&mut format_buf, max_fps)?;
|
|
464
517
|
let mut buffer_buf = Vec::new();
|
|
465
518
|
buffer_pod(&mut buffer_buf)?;
|
|
466
|
-
let mut
|
|
519
|
+
let mut cursor_buf = Vec::new();
|
|
520
|
+
cursor_pod(&mut cursor_buf)?;
|
|
521
|
+
let mut params = vec![
|
|
467
522
|
Pod::from_bytes(&format_buf).context("invalid EnumFormat pod")?,
|
|
468
523
|
Pod::from_bytes(&buffer_buf).context("invalid Buffers pod")?,
|
|
469
524
|
];
|
|
525
|
+
if wants_cursor_meta {
|
|
526
|
+
params.push(Pod::from_bytes(&cursor_buf).context("invalid Cursor Meta pod")?);
|
|
527
|
+
}
|
|
470
528
|
|
|
471
529
|
stream
|
|
472
530
|
.connect(
|
|
@@ -658,6 +716,7 @@ mod tests {
|
|
|
658
716
|
origin_x: 0,
|
|
659
717
|
origin_y: 0,
|
|
660
718
|
},
|
|
719
|
+
cursor_positions: Arc::new(AtomicBool::new(false)),
|
|
661
720
|
geometry: Arc::new(Mutex::new(None)),
|
|
662
721
|
frames: Arc::new(AtomicU64::new(0)),
|
|
663
722
|
dropped: Arc::new(AtomicU64::new(0)),
|
|
@@ -678,3 +737,33 @@ mod tests {
|
|
|
678
737
|
.expect("capture stopped and joined");
|
|
679
738
|
}
|
|
680
739
|
}
|
|
740
|
+
|
|
741
|
+
fn cursor_pod(buffer: &mut Vec<u8>) -> Result<()> {
|
|
742
|
+
use spa::pod::{object, property, Value};
|
|
743
|
+
let obj = object!(
|
|
744
|
+
spa::utils::SpaTypes::ObjectParamMeta,
|
|
745
|
+
spa::param::ParamType::Meta,
|
|
746
|
+
property!(
|
|
747
|
+
RawKey(spa::sys::SPA_PARAM_META_type),
|
|
748
|
+
Id,
|
|
749
|
+
RawKey(spa::sys::SPA_META_Cursor)
|
|
750
|
+
),
|
|
751
|
+
property!(
|
|
752
|
+
RawKey(spa::sys::SPA_PARAM_META_size),
|
|
753
|
+
Int,
|
|
754
|
+
(std::mem::size_of::<CursorWithBitmap>() + 256 * 256 * 4) as i32
|
|
755
|
+
)
|
|
756
|
+
);
|
|
757
|
+
PodSerializer::serialize(Cursor::new(buffer), &Value::Object(obj))
|
|
758
|
+
.map(|_| ())
|
|
759
|
+
.map_err(|e| anyhow::anyhow!("failed to build Cursor Meta pod: {e}"))
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
#[repr(C)]
|
|
763
|
+
struct CursorWithBitmap {
|
|
764
|
+
cursor: spa::sys::spa_meta_cursor,
|
|
765
|
+
bitmap: spa::sys::spa_meta_bitmap,
|
|
766
|
+
}
|
|
767
|
+
impl spa::buffer::meta::Metadata for CursorWithBitmap {
|
|
768
|
+
const META_TYPE: u32 = spa::sys::SPA_META_Cursor;
|
|
769
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//! Windows desktop capture is deferred; encoded sources never call this adapter.
|
|
2
|
+
use crate::convert::I420;
|
|
3
|
+
use crate::portal::PortalSession;
|
|
4
|
+
use anyhow::{bail, Result};
|
|
5
|
+
|
|
6
|
+
pub type FrameSink = Box<dyn Fn(I420, u64, Pixels) + Send + 'static>;
|
|
7
|
+
pub type Pixels<'a> = &'a mut dyn FnMut() -> Option<Vec<u8>>;
|
|
8
|
+
pub struct Capture;
|
|
9
|
+
|
|
10
|
+
pub fn unavailable_reason() -> &'static str {
|
|
11
|
+
"Windows display capture is not implemented in this build."
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn start(
|
|
15
|
+
_session: PortalSession,
|
|
16
|
+
_width: usize,
|
|
17
|
+
_height: usize,
|
|
18
|
+
_fps: u32,
|
|
19
|
+
_sink: FrameSink,
|
|
20
|
+
_status: Box<dyn Fn(bool, String) + Send>,
|
|
21
|
+
) -> Result<Capture> {
|
|
22
|
+
bail!("{}", unavailable_reason())
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//! Windows clipboard seam. This build never reads or writes the clipboard.
|
|
2
|
+
pub const MAX_CLIPBOARD_BYTES: usize = 256 * 1024;
|
|
3
|
+
pub fn read_or_explain() -> Result<(String, bool), String> {
|
|
4
|
+
Err("Windows clipboard transfer is not implemented in this build.".into())
|
|
5
|
+
}
|
|
6
|
+
pub fn write(_text: &str) -> anyhow::Result<()> {
|
|
7
|
+
anyhow::bail!("Windows clipboard transfer is not implemented in this build.")
|
|
8
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
//! Cursor metadata is independent of pixel damage and never painted into video.
|
|
2
|
+
use crate::protocol::{timestamp_us, CursorPosition};
|
|
3
|
+
use std::time::Instant;
|
|
4
|
+
|
|
5
|
+
pub type CursorSink = Box<dyn Fn(CursorPosition) + Send + 'static>;
|
|
6
|
+
|
|
7
|
+
pub struct Reporter {
|
|
8
|
+
sink: CursorSink,
|
|
9
|
+
last: Option<(i32, i32, bool)>,
|
|
10
|
+
bitmap_visible: bool,
|
|
11
|
+
}
|
|
12
|
+
impl Reporter {
|
|
13
|
+
pub fn new(sink: CursorSink) -> Self {
|
|
14
|
+
Self {
|
|
15
|
+
sink,
|
|
16
|
+
last: None,
|
|
17
|
+
bitmap_visible: true,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
pub fn update_spa(
|
|
21
|
+
&mut self,
|
|
22
|
+
meta: &pipewire::spa::sys::spa_meta_cursor,
|
|
23
|
+
bitmap: Option<&pipewire::spa::sys::spa_meta_bitmap>,
|
|
24
|
+
width: usize,
|
|
25
|
+
height: usize,
|
|
26
|
+
) {
|
|
27
|
+
// SPA id zero is no new data, not a visibility change.
|
|
28
|
+
if meta.id == 0 {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if let Some(bitmap) = bitmap.filter(|b| b.format != 0) {
|
|
32
|
+
self.bitmap_visible = bitmap.offset != 0;
|
|
33
|
+
}
|
|
34
|
+
let (x, y) = (meta.position.x, meta.position.y);
|
|
35
|
+
self.update(
|
|
36
|
+
x,
|
|
37
|
+
y,
|
|
38
|
+
self.bitmap_visible
|
|
39
|
+
&& x >= 0
|
|
40
|
+
&& y >= 0
|
|
41
|
+
&& (x as usize) < width
|
|
42
|
+
&& (y as usize) < height,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
pub fn update(&mut self, x: i32, y: i32, visible: bool) {
|
|
46
|
+
if self.last == Some((x, y, visible)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
self.last = Some((x, y, visible));
|
|
50
|
+
(self.sink)(CursorPosition {
|
|
51
|
+
x,
|
|
52
|
+
y,
|
|
53
|
+
visible,
|
|
54
|
+
timestamp_us: timestamp_us(Instant::now()),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[cfg(test)]
|
|
60
|
+
mod tests {
|
|
61
|
+
use super::*;
|
|
62
|
+
#[test]
|
|
63
|
+
fn spa_no_update_off_source_and_invisible_bitmap_are_distinct() {
|
|
64
|
+
use pipewire::spa::sys::{spa_meta_bitmap, spa_meta_cursor, spa_point, spa_rectangle};
|
|
65
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
66
|
+
let mut reporter = Reporter::new(Box::new(move |p| {
|
|
67
|
+
tx.send(p).unwrap();
|
|
68
|
+
}));
|
|
69
|
+
let mut meta = spa_meta_cursor {
|
|
70
|
+
id: 1,
|
|
71
|
+
flags: 0,
|
|
72
|
+
position: spa_point { x: 40, y: 60 },
|
|
73
|
+
hotspot: spa_point { x: 2, y: 3 },
|
|
74
|
+
bitmap_offset: 0,
|
|
75
|
+
};
|
|
76
|
+
reporter.update_spa(&meta, None, 800, 600);
|
|
77
|
+
meta.id = 0;
|
|
78
|
+
reporter.update_spa(&meta, None, 800, 600);
|
|
79
|
+
assert_eq!(rx.try_iter().count(), 1);
|
|
80
|
+
meta.id = 1;
|
|
81
|
+
meta.position.x = -1;
|
|
82
|
+
reporter.update_spa(&meta, None, 800, 600);
|
|
83
|
+
assert!(!rx.try_recv().unwrap().visible);
|
|
84
|
+
meta.position.x = 40;
|
|
85
|
+
let bitmap = spa_meta_bitmap {
|
|
86
|
+
format: 1,
|
|
87
|
+
size: spa_rectangle {
|
|
88
|
+
width: 16,
|
|
89
|
+
height: 16,
|
|
90
|
+
},
|
|
91
|
+
stride: 64,
|
|
92
|
+
offset: 0,
|
|
93
|
+
};
|
|
94
|
+
reporter.update_spa(&meta, Some(&bitmap), 800, 600);
|
|
95
|
+
assert!(!rx.try_recv().unwrap().visible);
|
|
96
|
+
reporter.update_spa(&meta, None, 800, 600);
|
|
97
|
+
assert!(rx.try_recv().is_err(), "no new bitmap retains invisibility");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn reports_initial_motion_and_visibility_but_not_unchanged_polls() {
|
|
102
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
103
|
+
let mut reporter = Reporter::new(Box::new(move |p| {
|
|
104
|
+
tx.send(p).unwrap();
|
|
105
|
+
}));
|
|
106
|
+
reporter.update(10, 20, true);
|
|
107
|
+
reporter.update(10, 20, true);
|
|
108
|
+
reporter.update(30, 40, true);
|
|
109
|
+
reporter.update(30, 40, false);
|
|
110
|
+
let notices: Vec<_> = rx.try_iter().collect();
|
|
111
|
+
assert_eq!(notices.len(), 3);
|
|
112
|
+
assert_eq!((notices[1].x, notices[1].y), (30, 40));
|
|
113
|
+
assert!(!notices[2].visible);
|
|
114
|
+
assert!(notices
|
|
115
|
+
.windows(2)
|
|
116
|
+
.all(|p| p[0].timestamp_us <= p[1].timestamp_us));
|
|
117
|
+
}
|
|
118
|
+
}
|