@desklink/host 0.3.0 → 0.4.0

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.
Files changed (47) hide show
  1. package/NOTICE +3 -0
  2. package/README.md +92 -23
  3. package/bin/desklink-host.mjs +14 -5
  4. package/dist/bridge.d.ts +3 -4
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +18 -7
  7. package/dist/bridge.js.map +1 -1
  8. package/dist/engineProcess.d.ts +6 -1
  9. package/dist/engineProcess.d.ts.map +1 -1
  10. package/dist/engineProcess.js +34 -5
  11. package/dist/engineProcess.js.map +1 -1
  12. package/dist/protocol.d.ts +35 -2
  13. package/dist/protocol.d.ts.map +1 -1
  14. package/dist/protocol.js.map +1 -1
  15. package/dist/resolveEngine.d.ts +6 -0
  16. package/dist/resolveEngine.d.ts.map +1 -1
  17. package/dist/resolveEngine.js +45 -6
  18. package/dist/resolveEngine.js.map +1 -1
  19. package/docs/BOUNDARY.md +8 -8
  20. package/docs/PROTOCOL.md +185 -45
  21. package/engine/Cargo.lock +2 -2
  22. package/engine/Cargo.toml +7 -12
  23. package/engine/build.rs +62 -5
  24. package/engine/native/agent_indicator.h +57 -0
  25. package/engine/native/agent_overlay_mac.m +51 -4
  26. package/engine/native/agent_overlay_wayland.c +189 -35
  27. package/engine/native/agent_overlay_x11.c +186 -55
  28. package/engine/src/capture.rs +96 -5
  29. package/engine/src/capture_win.rs +23 -0
  30. package/engine/src/clipboard.rs +174 -0
  31. package/engine/src/clipboard_win.rs +8 -0
  32. package/engine/src/cursor.rs +118 -0
  33. package/engine/src/indicator.rs +271 -5
  34. package/engine/src/input_win.rs +91 -0
  35. package/engine/src/keymap_win.rs +39 -0
  36. package/engine/src/mac.rs +28 -6
  37. package/engine/src/main.rs +167 -25
  38. package/engine/src/peer.rs +10 -4
  39. package/engine/src/portal.rs +42 -4
  40. package/engine/src/portal_win.rs +37 -0
  41. package/engine/src/protocol.rs +56 -10
  42. package/engine/src/session.rs +749 -203
  43. package/engine/src/win.rs +110 -0
  44. package/engine/src/x11.rs +44 -29
  45. package/engine/src/x11_mac.rs +4 -0
  46. package/examples/reference-client.html +3 -5
  47. package/package.json +4 -4
@@ -3,18 +3,31 @@
3
3
  #include <stdio.h>
4
4
  #include <stdlib.h>
5
5
  #include <unistd.h>
6
+ #include <string.h>
6
7
 
7
8
  @interface AgentOverlayView : NSView {
8
9
  NSPoint target, cursor;
9
10
  CFTimeInterval clickAt, typeAt, endingAt;
10
11
  BOOL positioned;
12
+ BOOL pointMode, pointVisible;
13
+ CFTimeInterval pointDeadline;
14
+ NSString *pointLabel;
11
15
  }
12
16
  - (void)event:(char)kind x:(double)x y:(double)y;
13
17
  - (void)end;
18
+ - (void)enablePointMode;
19
+ - (void)pointX:(double)x y:(double)y timeout:(unsigned)timeout label:(NSString *)label;
14
20
  @end
15
21
 
16
22
  @implementation AgentOverlayView
17
23
  - (BOOL)isOpaque { return NO; }
24
+ - (void)enablePointMode { pointMode = YES; }
25
+ - (void)pointX:(double)x y:(double)y timeout:(unsigned)timeout label:(NSString *)label {
26
+ cursor = NSMakePoint(x, self.bounds.size.height-y);
27
+ pointLabel = label; pointVisible = YES;
28
+ pointDeadline = CACurrentMediaTime() + timeout/1000.0;
29
+ [self setNeedsDisplay:YES];
30
+ }
18
31
  - (void)event:(char)kind x:(double)x y:(double)y {
19
32
  if (kind == 'S') { [self end]; return; }
20
33
  if (x >= 0 && y >= 0) {
@@ -27,8 +40,12 @@
27
40
  if (kind == 'T') typeAt = now;
28
41
  [self setNeedsDisplay:YES];
29
42
  }
30
- - (void)end { endingAt = CACurrentMediaTime(); }
43
+ - (void)end { if (pointMode) { [NSApp terminate:nil]; return; } endingAt = CACurrentMediaTime(); }
31
44
  - (void)tick {
45
+ if (pointMode) {
46
+ if (pointVisible && CACurrentMediaTime() >= pointDeadline) { pointVisible = NO; [self setNeedsDisplay:YES]; }
47
+ return;
48
+ }
32
49
  if (endingAt && CACurrentMediaTime() - endingAt > 0.7) { [NSApp terminate:nil]; return; }
33
50
  cursor.x += (target.x - cursor.x) * 0.24;
34
51
  cursor.y += (target.y - cursor.y) * 0.24;
@@ -36,6 +53,23 @@
36
53
  }
37
54
  - (void)drawRect:(NSRect)rect {
38
55
  [[NSColor clearColor] set]; NSRectFillUsingOperation(rect, NSCompositingOperationCopy);
56
+ if (pointMode) {
57
+ if (!pointVisible) return;
58
+ [[NSColor colorWithCalibratedRed:0.30 green:0.62 blue:0.82 alpha:1] set];
59
+ NSBezierPath *ring = [NSBezierPath bezierPathWithOvalInRect:NSMakeRect(cursor.x-18, cursor.y-18, 36, 36)];
60
+ ring.lineWidth = 4; [ring stroke];
61
+ [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(cursor.x-3,cursor.y-3,6,6)] fill];
62
+ if (pointLabel.length) {
63
+ NSDictionary *style = @{NSFontAttributeName: [NSFont systemFontOfSize:13], NSForegroundColorAttributeName: NSColor.whiteColor};
64
+ NSSize size = [pointLabel sizeWithAttributes:style];
65
+ CGFloat x = fmax(0, fmin(cursor.x+30, self.bounds.size.width-size.width-16));
66
+ CGFloat y = fmax(0, fmin(cursor.y-14, self.bounds.size.height-28));
67
+ [[NSColor colorWithCalibratedRed:0.07 green:0.19 blue:0.28 alpha:1] set];
68
+ [[NSBezierPath bezierPathWithRoundedRect:NSMakeRect(x,y,size.width+16,28) xRadius:4 yRadius:4] fill];
69
+ [pointLabel drawAtPoint:NSMakePoint(x+8,y+6) withAttributes:style];
70
+ }
71
+ return;
72
+ }
39
73
  CFTimeInterval now = CACurrentMediaTime();
40
74
  CGFloat edge = endingAt ? fmax(0, 1 - (now-endingAt)/0.7) : 1;
41
75
  if (edge > 0) {
@@ -95,7 +129,7 @@ unsigned char *desklink_image_bgra(CGImageRef image, size_t *length, size_t *str
95
129
  return pixels;
96
130
  }
97
131
 
98
- int desklink_agent_overlay_main(int display_id) {
132
+ static int overlay_main(int display_id, BOOL point_mode) {
99
133
  @autoreleasepool {
100
134
  [NSApplication sharedApplication];
101
135
  [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
@@ -111,14 +145,24 @@ int desklink_agent_overlay_main(int display_id) {
111
145
  window.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary;
112
146
  AgentOverlayView *view = [[AgentOverlayView alloc] initWithFrame:NSMakeRect(0,0,screen.frame.size.width,screen.frame.size.height)];
113
147
  window.contentView = view;
148
+ if (point_mode) [view enablePointMode];
114
149
  NSPoint mouse = [NSEvent mouseLocation];
115
- [view event:'M' x:mouse.x-screen.frame.origin.x y:NSMaxY(screen.frame)-mouse.y];
150
+ if (!point_mode) [view event:'M' x:mouse.x-screen.frame.origin.x y:NSMaxY(screen.frame)-mouse.y];
116
151
  [window orderFrontRegardless];
117
152
  puts("READY"); fflush(stdout);
118
153
  [NSTimer scheduledTimerWithTimeInterval:1.0/60 target:view selector:@selector(tick) userInfo:nil repeats:YES];
119
154
  dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
120
- char line[128];
155
+ char line[512];
121
156
  while (fgets(line, sizeof(line), stdin)) {
157
+ if (point_mode) {
158
+ double x, y; unsigned timeout; int offset = 0;
159
+ if (sscanf(line, "P %lf %lf %u %n", &x, &y, &timeout, &offset) == 3 && offset) {
160
+ line[strcspn(line, "\n")] = 0;
161
+ NSString *label = [NSString stringWithUTF8String:line+offset];
162
+ dispatch_async(dispatch_get_main_queue(), ^{ [view pointX:x y:y timeout:timeout label:label]; });
163
+ }
164
+ continue;
165
+ }
122
166
  char kind; double x = 0, y = 0;
123
167
  if (sscanf(line, "%c %lf %lf", &kind, &x, &y) >= 1) {
124
168
  dispatch_async(dispatch_get_main_queue(), ^{ [view event:kind x:x y:y]; });
@@ -131,3 +175,6 @@ int desklink_agent_overlay_main(int display_id) {
131
175
  }
132
176
  return 0;
133
177
  }
178
+
179
+ int desklink_agent_overlay_main(int display_id) { return overlay_main(display_id, NO); }
180
+ int desklink_point_overlay_main(int display_id) { return overlay_main(display_id, YES); }
@@ -1,5 +1,5 @@
1
1
  // Minimal wlroots layer-shell surface: transparent, unfocusable and input-empty.
2
- // Only the portal path uses it; X11 uses a cursor sprite instead.
2
+ // Portal capture masking is implemented by capture.rs::IndicatorMask.
3
3
  // Protocol interface names/signatures from wlr-layer-shell-unstable-v1:
4
4
  // Copyright (c) 2017 Drew DeVault. Permission is hereby granted to use, copy,
5
5
  // modify and distribute this protocol for any purpose without fee, provided
@@ -14,9 +14,13 @@
14
14
  #include <unistd.h>
15
15
  #include <fcntl.h>
16
16
  #include <math.h>
17
+ #include "agent_indicator.h"
17
18
  #include <stdio.h>
18
19
  #include <stdlib.h>
19
20
  #include <string.h>
21
+ #include <fontconfig/fontconfig.h>
22
+ #include <ft2build.h>
23
+ #include FT_FREETYPE_H
20
24
 
21
25
  // Layer-shell v1 protocol (Drew DeVault, 2017; MIT license).
22
26
  static const struct wl_interface layer_surface_interface;
@@ -35,6 +39,44 @@ static const struct wl_message surface_requests[] = {
35
39
  static const struct wl_message surface_events[] = { {"configure", "uuu", NULL}, {"closed", "", NULL} };
36
40
  static const struct wl_interface layer_surface_interface = {"zwlr_layer_surface_v1", 1, 8, surface_requests, 2, surface_events};
37
41
 
42
+ /* xdg-output wire signatures: Copyright (c) 2017 Red Hat Inc.
43
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
44
+ * of this software and associated documentation files (the "Software"), to deal
45
+ * in the Software without restriction, including without limitation the rights
46
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
47
+ * copies of the Software, and to permit persons to whom the Software is
48
+ * furnished to do so, subject to the following conditions:
49
+ * The above copyright notice and this permission notice shall be included in
50
+ * all copies or substantial portions of the Software.
51
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
52
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
53
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
54
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
55
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
56
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
57
+ * SOFTWARE.
58
+ */
59
+ // xdg-output v2 supplies compositor logical coordinates, including scaled outputs.
60
+ static const struct wl_interface xdg_output_interface;
61
+ static const struct wl_interface *xdg_types[]={&xdg_output_interface,&wl_output_interface};
62
+ static const struct wl_message xdg_requests[]={{"destroy","",NULL},{"get_xdg_output","no",xdg_types}};
63
+ static const struct wl_interface xdg_manager_interface={"zxdg_output_manager_v1",2,2,xdg_requests,0,NULL};
64
+ static const struct wl_message output_requests[]={{"destroy","",NULL}};
65
+ static const struct wl_message output_events[]={{"logical_position","ii",NULL},{"logical_size","ii",NULL},{"done","",NULL},{"name","2s",NULL},{"description","2s",NULL}};
66
+ static const struct wl_interface xdg_output_interface={"zxdg_output_v1",2,1,output_requests,5,output_events};
67
+ struct Output { struct wl_output *handle; struct wl_proxy *logical; int x,y,w,h,valid,scale; };
68
+ static void output_geometry(void *data,struct wl_output *p,int32_t x,int32_t y,int32_t pw,int32_t ph,int32_t sub,const char *make,const char *model,int32_t transform)
69
+ {(void)data;(void)p;(void)x;(void)y;(void)pw;(void)ph;(void)sub;(void)make;(void)model;(void)transform;}
70
+ static void output_mode(void *data,struct wl_output *p,uint32_t flags,int32_t w,int32_t h,int32_t refresh){(void)data;(void)p;(void)flags;(void)w;(void)h;(void)refresh;}
71
+ static void output_done(void *data,struct wl_output *p){(void)data;(void)p;}
72
+ static void output_scale(void *data,struct wl_output *p,int32_t scale){(void)p;((struct Output *)data)->scale=scale;}
73
+ static const struct wl_output_listener output_listener={.geometry=output_geometry,.mode=output_mode,.done=output_done,.scale=output_scale};
74
+ 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;}
75
+ 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;}
76
+ static void logical_done(void *data,struct wl_proxy *p){(void)data;(void)p;}
77
+ static void logical_string(void *data,struct wl_proxy *p,const char *s){(void)data;(void)p;(void)s;}
78
+ 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};
79
+
38
80
  int desklink_wayland_geometry(int source_w, int source_h, int surface_w, int surface_h,
39
81
  double px, double py, double *x, double *y) {
40
82
  if (source_w<=0||source_h<=0||surface_w<=0||surface_h<=0) return 0;
@@ -50,14 +92,22 @@ struct Overlay {
50
92
  struct wl_display *display;
51
93
  struct wl_compositor *compositor;
52
94
  struct wl_shm *shm;
53
- struct wl_proxy *shell, *layer;
95
+ struct wl_proxy *shell, *layer, *output_manager;
96
+ struct Output outputs[32]; int output_count;
54
97
  struct wl_surface *surface;
55
98
  struct Buffer buffers[2];
56
- int width, height, source_w, source_h, configured, closed;
99
+ int width, height, scale, source_w, source_h, configured, closed;
57
100
  };
58
101
  static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec+t.tv_nsec/1e9; }
59
102
  static void registry_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
60
103
  struct Overlay *o=data;
104
+ if (!strcmp(interface,"wl_output") && o->output_count<32) {
105
+ struct Output *out=&o->outputs[o->output_count++];
106
+ out->handle=wl_registry_bind(registry,name,&wl_output_interface,version<2?version:2);
107
+ out->scale=1;
108
+ wl_output_add_listener(out->handle,&output_listener,out);
109
+ }
110
+ if (!strcmp(interface,"zxdg_output_manager_v1")) o->output_manager=wl_registry_bind(registry,name,&xdg_manager_interface,version<2?version:2);
61
111
  if (!strcmp(interface,"wl_compositor")) o->compositor=wl_registry_bind(registry,name,&wl_compositor_interface,version<4?version:4);
62
112
  if (!strcmp(interface,"wl_shm")) o->shm=wl_registry_bind(registry,name,&wl_shm_interface,1);
63
113
  if (!strcmp(interface,"zwlr_layer_shell_v1")) o->shell=wl_registry_bind(registry,name,&shell_interface,1);
@@ -65,7 +115,11 @@ static void registry_global(void *data, struct wl_registry *registry, uint32_t n
65
115
  static void registry_remove(void *data, struct wl_registry *registry, uint32_t name) { (void)data;(void)registry;(void)name; }
66
116
  static const struct wl_registry_listener registry_listener={registry_global,registry_remove};
67
117
  static void configured(void *data, struct wl_proxy *layer, uint32_t serial, uint32_t w, uint32_t h) {
68
- struct Overlay *o=data; o->width=(int)w; o->height=(int)h; o->configured=1;
118
+ struct Overlay *o=data;
119
+ // A source/layout change invalidates this helper; never write new dimensions
120
+ // into buffers allocated for the old output size.
121
+ if(o->configured && (o->width!=(int)w || o->height!=(int)h))o->closed=1;
122
+ o->width=(int)w; o->height=(int)h; o->configured=1;
69
123
  wl_proxy_marshal_flags(layer,6,NULL,1,0,serial);
70
124
  }
71
125
  static void closed(void *data, struct wl_proxy *layer) { (void)layer; ((struct Overlay *)data)->closed=1; }
@@ -74,13 +128,14 @@ static void buffer_release(void *data, struct wl_buffer *buffer) { (void)buffer;
74
128
  static const struct wl_buffer_listener buffer_listener={buffer_release};
75
129
 
76
130
  static int create_buffer(struct Overlay *o, struct Buffer *b) {
77
- b->length=(size_t)o->width*o->height*4;
131
+ int w=o->width*o->scale,h=o->height*o->scale;
132
+ b->length=(size_t)w*h*4;
78
133
  int fd=memfd_create("desklink-agent-overlay",MFD_CLOEXEC);
79
134
  if (fd<0 || ftruncate(fd,b->length)) { if(fd>=0)close(fd); return -1; }
80
135
  b->pixels=mmap(NULL,b->length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
81
136
  if (b->pixels==MAP_FAILED) {close(fd);return -1;}
82
137
  struct wl_shm_pool *pool=wl_shm_create_pool(o->shm,fd,(int)b->length);
83
- b->handle=wl_shm_pool_create_buffer(pool,0,o->width,o->height,o->width*4,WL_SHM_FORMAT_ARGB8888);
138
+ b->handle=wl_shm_pool_create_buffer(pool,0,w,h,w*4,WL_SHM_FORMAT_ARGB8888);
84
139
  wl_shm_pool_destroy(pool); close(fd);
85
140
  wl_buffer_add_listener(b->handle,&buffer_listener,b);
86
141
  return 0;
@@ -89,12 +144,15 @@ static void blend(uint32_t *pixel, unsigned alpha, unsigned rgb) {
89
144
  // premultiplied ARGB8888: the compositor blends this over the desktop.
90
145
  *pixel=(alpha<<24)|(((rgb>>16)&255)*alpha/255<<16)|(((rgb>>8)&255)*alpha/255<<8)|((rgb&255)*alpha/255);
91
146
  }
92
- static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double click, double typed, double ending, int positioned) {
93
- double t=now(), opacity=ending?fmax(0,1-(t-ending)/0.6):1;
147
+ static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double click, double typed, double ending, double started, double cue_started) {
148
+ double t=now(), opacity=ending?fmax(0,1-(t-ending)/0.6):fmin(1,(t-started)/0.18);
149
+ // The cue fades in from its first position, not from the session's start.
150
+ double cue=ending?opacity:fmin(1,(t-cue_started)/0.18);
94
151
  memset(b->pixels,0,b->length);
95
- int w=o->width,h=o->height;
96
- for (int edge=0;edge<7;edge++) {
97
- unsigned alpha=(unsigned)((7-edge)*1.8*opacity);
152
+ // Draw in buffer pixels; the cue itself is specified in logical pixels.
153
+ int s=o->scale,w=o->width*s,h=o->height*s;x*=s;y*=s;
154
+ for (int edge=0;edge<7*s;edge++) {
155
+ unsigned alpha=(unsigned)((7-(double)edge/s)*1.8*opacity);
98
156
  for(int px=edge;px<w-edge;px++) {
99
157
  blend(&b->pixels[(size_t)edge*w+px],alpha,0x4c9ed0);
100
158
  blend(&b->pixels[(size_t)(h-1-edge)*w+px],alpha,0x4c9ed0);
@@ -104,29 +162,95 @@ static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double
104
162
  blend(&b->pixels[(size_t)py*w+w-1-edge],alpha,0x4c9ed0);
105
163
  }
106
164
  }
107
- int left=fmax(0,x-55),right=fmin(w,x+55),top=fmax(0,y-55),bottom=fmin(h,y+55);
108
- if(positioned)for(int py=top;py<bottom;py++)for(int px=left;px<right;px++) {
109
- double distance=hypot(px-x,py-y);
110
- double ring=fmax(0,1-fabs(distance-13)/5)*0.18;
111
- ring+=fmax(0,1-fabs(distance-23)/7)*0.06;
112
- if(click && t-click<0.55)ring+=fmax(0,1-fabs(distance-(12+37*(t-click)/0.55))/3)*0.42*(1-(t-click)/0.55);
113
- if(typed && t-typed<0.4)
114
- ring+=fmax(0,1-fabs(distance-(8+15*(t-typed)/0.4))/3)*0.22*(1-(t-typed)/0.4);
115
- unsigned alpha=(unsigned)(fmin(1,ring*opacity)*255);
116
- if(alpha)blend(&b->pixels[(size_t)py*w+px],alpha,0x4c9ed0);
165
+ int left=fmax(0,x-40*s),right=fmin(w,x+40*s),top=fmax(0,y-40*s),bottom=fmin(h,y+40*s);
166
+ if(cue_started)for(int py=top;py<bottom;py++)for(int px=left;px<right;px++) {
167
+ // Capture keeps the pointer's own pixels live (IndicatorMask), so the
168
+ // tip stays clear; the real cursor sits there anyway.
169
+ if(hypot((px-x)/s,(py-y)/s)<6)continue;
170
+ uint32_t src=indicator_pixel((px-x)/s,(py-y)/s,cue,
171
+ click?(t-click)/0.55:-1,typed?(t-typed)/0.4:-1), *dst=&b->pixels[(size_t)py*w+px];
172
+ // Premultiplied source-over, keeping the edge glow under the cue.
173
+ unsigned inverse=255-(src>>24);
174
+ if(src)*dst=src+((((*dst>>24)&255)*inverse/255)<<24)+((((*dst>>16)&255)*inverse/255)<<16)
175
+ +((((*dst>>8)&255)*inverse/255)<<8)+((*dst&255)*inverse/255);
176
+ }
177
+ }
178
+
179
+ static FT_Face point_font(FT_Library *library) {
180
+ if(FT_Init_FreeType(library))return NULL;
181
+ FcPattern *pattern=FcNameParse((const FcChar8 *)"monospace");
182
+ if(!pattern)return NULL;
183
+ FcConfigSubstitute(NULL,pattern,FcMatchPattern);FcDefaultSubstitute(pattern);
184
+ FcResult result;FcPattern *match=FcFontMatch(NULL,pattern,&result);FcPatternDestroy(pattern);
185
+ FcChar8 *path=NULL;FT_Face face=NULL;
186
+ if(match && FcPatternGetString(match,FC_FILE,0,&path)==FcResultMatch)
187
+ if(!FT_New_Face(*library,(const char *)path,0,&face))FT_Set_Pixel_Sizes(face,0,13);
188
+ if(match)FcPatternDestroy(match);
189
+ return face;
190
+ }
191
+ static void draw_point(struct Overlay *o,struct Buffer *b,double x,double y,const char *label,FT_Face font,int visible) {
192
+ memset(b->pixels,0,b->length);if(!visible)return;
193
+ int w=o->width,h=o->height;
194
+ 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++) {
195
+ double d=hypot(px-x,py-y);
196
+ if(fabs(d-18)<=2 || d<=3)b->pixels[(size_t)py*w+px]=0xff4c9ed0;
197
+ }
198
+ if(!*label)return;
199
+ int badge_w=16;
200
+ 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;
201
+ badge_w=fmin(w,badge_w);int bx=fmax(0,fmin(x+30,w-badge_w)),by=fmax(0,fmin(y-14,h-28));
202
+ 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;
203
+ int pen=bx+8;
204
+ for(const char *p=label;*p;p++)if(!FT_Load_Char(font,(unsigned char)*p,FT_LOAD_RENDER)) {
205
+ FT_GlyphSlot g=font->glyph;
206
+ for(unsigned row=0;row<g->bitmap.rows;row++)for(unsigned col=0;col<g->bitmap.width;col++) {
207
+ int px=pen+g->bitmap_left+col,py=by+18-g->bitmap_top+row;
208
+ if(px<0||px>=w||py<0||py>=h)continue;
209
+ unsigned a=g->bitmap.buffer[row*g->bitmap.pitch+col];
210
+ unsigned r=(18*(255-a)+255*a)/255,green=(48*(255-a)+255*a)/255,blue=(71*(255-a)+255*a)/255;
211
+ b->pixels[(size_t)py*w+px]=0xff000000|(r<<16)|(green<<8)|blue;
212
+ }
213
+ pen+=g->advance.x>>6;
117
214
  }
118
215
  }
119
216
 
120
- int desklink_agent_overlay_wayland(int source_w, int source_h) {
121
- struct Overlay o={.source_w=source_w,.source_h=source_h};
217
+ static int overlay_main(int source_w,int source_h,int point_mode,int origin_known,int origin_x,int origin_y) {
218
+
219
+ struct Overlay o={.source_w=source_w,.source_h=source_h,.scale=1};
122
220
  o.display=wl_display_connect(NULL);
123
221
  if(!o.display)return 2;
124
222
  struct wl_registry *registry=wl_display_get_registry(o.display);
125
223
  wl_registry_add_listener(registry,&registry_listener,&o);
126
224
  wl_display_roundtrip(o.display);
127
- if(!o.compositor||!o.shm||!o.shell){wl_display_disconnect(o.display);return 2;}
225
+ if(!o.shell){puts("layer_shell_unavailable");fflush(stdout);wl_display_disconnect(o.display);return 2;}
226
+ if(!o.compositor||!o.shm){wl_display_disconnect(o.display);return 2;}
227
+ struct wl_output *selected=NULL;int matches=0;
228
+ if(point_mode) {
229
+ if(o.output_manager) {
230
+ for(int i=0;i<o.output_count;i++) {
231
+ struct Output *out=&o.outputs[i];
232
+ out->logical=wl_proxy_marshal_flags(o.output_manager,1,&xdg_output_interface,wl_proxy_get_version(o.output_manager),0,NULL,out->handle);
233
+ wl_proxy_add_listener(out->logical,logical_listener,out);
234
+ }
235
+ if(wl_display_roundtrip(o.display)<0)return 2;
236
+ }
237
+ for(int i=0;i<o.output_count;i++) {
238
+ struct Output *out=&o.outputs[i];
239
+ 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++;}
240
+ }
241
+ if(matches>1)selected=NULL;
242
+ if(!selected && o.output_count==1 && (!origin_known || !o.output_manager))selected=o.outputs[0].handle;
243
+ if(!selected){puts("selected_output_unavailable");fflush(stdout);wl_display_disconnect(o.display);return 2;}
244
+ }
245
+ if(!point_mode && wl_proxy_get_version((struct wl_proxy *)o.compositor)>=3) {
246
+ // Render the cue at the densest output's integer scale so it stays crisp on HiDPI.
247
+ if(wl_display_roundtrip(o.display)<0)return 2;
248
+ for(int i=0;i<o.output_count;i++)if(o.outputs[i].scale>o.scale)o.scale=o.outputs[i].scale<4?o.outputs[i].scale:4;
249
+ }
250
+ FT_Library library=NULL;FT_Face font=point_mode?point_font(&library):NULL;
251
+ if(point_mode&&!font){wl_display_disconnect(o.display);return 2;}
128
252
  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,NULL,3,"desklink-agent");
253
+ 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
254
  wl_proxy_add_listener(o.layer,layer_listener,&o);
131
255
  uint32_t anchor,keyboard_focus,empty_input;
132
256
  desklink_wayland_policy(&anchor,&keyboard_focus,&empty_input);
@@ -139,15 +263,17 @@ int desklink_agent_overlay_wayland(int source_w, int source_h) {
139
263
  wl_surface_set_input_region(o.surface,empty);
140
264
  wl_region_destroy(empty);
141
265
  }
266
+ if(o.scale>1)wl_surface_set_buffer_scale(o.surface,o.scale);
142
267
  wl_surface_commit(o.surface);
143
268
  while(!o.configured&&!o.closed&&wl_display_dispatch(o.display)>=0) {}
144
269
  if(o.closed||o.width<=0||o.height<=0){wl_display_disconnect(o.display);return 2;}
145
270
  if(create_buffer(&o,&o.buffers[0])||create_buffer(&o,&o.buffers[1])){wl_display_disconnect(o.display);return 2;}
146
271
  int flags=fcntl(STDIN_FILENO,F_GETFL);fcntl(STDIN_FILENO,F_SETFL,flags|O_NONBLOCK);
147
272
  puts("READY");fflush(stdout);
148
- double tx=0,ty=0,x=0,y=0,click=0,typed=0,ending=0;
273
+ double tx=0,ty=0,x=0,y=0,click=0,typed=0,ending=0,started=0,cue_started=0;
149
274
  int active=0,positioned=0;
150
- char pending[128];size_t used=0;
275
+ double deadline=0;char label[97]={0};int dirty=0,visible=0;
276
+ char pending[512];size_t used=0;
151
277
  while(!o.closed && (!ending||now()-ending<0.6)) {
152
278
  struct pollfd fds[2]={{wl_display_get_fd(o.display),POLLIN,0},{STDIN_FILENO,POLLIN|POLLHUP,0}};
153
279
  wl_display_flush(o.display);
@@ -155,31 +281,43 @@ int desklink_agent_overlay_wayland(int source_w, int source_h) {
155
281
  if(fds[0].revents&POLLIN && wl_display_dispatch(o.display)<0)break;
156
282
  else wl_display_dispatch_pending(o.display);
157
283
  char bytes[128];ssize_t n=read(STDIN_FILENO,bytes,sizeof(bytes));
158
- if(n==0&&!ending)ending=now();
284
+ if(n==0){if(point_mode)break;if(!ending)ending=now();}
159
285
  for(ssize_t i=0;i<n;i++) {
160
286
  if(bytes[i]=='\n') {
161
287
  pending[used]=0;char kind=0;double px=0,py=0;
162
288
  if(sscanf(pending,"%c %lf %lf",&kind,&px,&py)>=1) {
163
- if(kind=='S')ending=now();
289
+ if(point_mode && kind=='P') {
290
+ unsigned timeout=0;int offset=0;
291
+ if(sscanf(pending,"P %lf %lf %u %n",&px,&py,&timeout,&offset)==3 && offset && timeout>=1 && timeout<=120000 && strlen(pending+offset)<=96) {
292
+ desklink_wayland_geometry(o.source_w,o.source_h,o.width,o.height,px,py,&x,&y);
293
+ snprintf(label,sizeof(label),"%s",pending+offset);deadline=now()+timeout/1000.0;active=1;dirty=1;visible=1;
294
+ }
295
+ }
296
+ else if(kind=='S')ending=now();
164
297
  else if(kind=='M'||kind=='C'||kind=='T'||kind=='A') {
165
298
  if(px>=0&&py>=0) {
166
299
  if(desklink_wayland_geometry(o.source_w,o.source_h,o.width,o.height,px,py,&tx,&ty)
167
- && !positioned){x=tx;y=ty;positioned=1;}
300
+ && !positioned){x=tx;y=ty;positioned=1;cue_started=now();}
168
301
  }
302
+ if(!active)started=now();
169
303
  active=1;
170
- if(kind=='C')click=now();if(kind=='T')typed=now();
304
+ if(kind=='C')click=now();
305
+ if(kind=='T')typed=now();
171
306
  }
172
307
  }
173
308
  used=0;
174
309
  }else if(used<sizeof(pending)-1)pending[used++]=bytes[i];
175
310
  }
176
- if(!active)continue;
177
- x+=(tx-x)*0.24;y+=(ty-y)*0.24;
311
+ if(point_mode && visible && now()>=deadline){visible=0;dirty=1;}
312
+ if(!active || (point_mode&&!dirty))continue;
313
+ if(!point_mode){x+=(tx-x)*0.24;y+=(ty-y)*0.24;}
178
314
  for(int i=0;i<2;i++)if(!o.buffers[i].busy) {
179
- draw(&o,&o.buffers[i],x,y,click,typed,ending,positioned);
315
+ if(point_mode)draw_point(&o,&o.buffers[i],x,y,label,font,visible);
316
+ else draw(&o,&o.buffers[i],x,y,click,typed,ending,started,cue_started);
317
+ dirty=0;
180
318
  o.buffers[i].busy=1;
181
319
  wl_surface_attach(o.surface,o.buffers[i].handle,0,0);
182
- wl_surface_damage_buffer(o.surface,0,0,o.width,o.height);
320
+ wl_surface_damage_buffer(o.surface,0,0,o.width*o.scale,o.height*o.scale);
183
321
  wl_surface_commit(o.surface);
184
322
  break;
185
323
  }
@@ -187,6 +325,22 @@ int desklink_agent_overlay_wayland(int source_w, int source_h) {
187
325
  wl_surface_attach(o.surface,NULL,0,0);wl_surface_commit(o.surface);wl_display_roundtrip(o.display);
188
326
  for(int i=0;i<2;i++){wl_buffer_destroy(o.buffers[i].handle);munmap(o.buffers[i].pixels,o.buffers[i].length);}
189
327
  wl_proxy_destroy(o.layer);wl_surface_destroy(o.surface);wl_proxy_destroy(o.shell);
328
+ if(font)FT_Done_Face(font);
329
+ if(library)FT_Done_FreeType(library);
190
330
  wl_display_disconnect(o.display);
191
331
  return 0;
192
332
  }
333
+
334
+ int desklink_agent_overlay_wayland(int w,int h){return overlay_main(w,h,0,0,0,0);}
335
+ int desklink_point_overlay_wayland(int w,int h,int known,int x,int y){return overlay_main(w,h,1,known,x,y);}
336
+
337
+ // Read-only capability probe for idempotent clear before a helper exists.
338
+ int desklink_wayland_layer_shell_available(void) {
339
+ struct Overlay o={0};o.display=wl_display_connect(NULL);
340
+ if(!o.display)return -1;
341
+ struct wl_registry *registry=wl_display_get_registry(o.display);
342
+ wl_registry_add_listener(registry,&registry_listener,&o);
343
+ int ok=wl_display_roundtrip(o.display)>=0;
344
+ int result=ok?(o.shell!=NULL):-1;
345
+ wl_display_disconnect(o.display);return result;
346
+ }