summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 13:00:53 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 13:00:53 +0300
commitb20263cf9b6d85c7e5919497b5d36b4c81463b15 (patch)
treed46d216fdde20dc36023a9b5ef285e8ba4bf69fd
parent2984d50b78e57c35c068136ca9e65416f193d21f (diff)
feat: implement X11 and Cocoa drop-targets, popups, and selection gaps
Resolve gaps identified in yasl-gaps-report.md: - X11: - Add XDND v5 file drop support and text/uri-list parsing (YASL_INPUT_DROP_FILES) - Dispatch surface drop-target events (YASL_DRAG_OVER, YASL_DRAG_LEAVE, YASL_DRAG_DROP, YASL_DROP_OVER, YASL_DROP_LEAVE) - Implement override-redirect popups (surface_create_popup, surface_popup_reposition) - Support drag images with override-redirect icon window - Set YASL_CAP_POPUPS capability - Handle XIHierarchyChanged reset cleanly - Cocoa: - Dispatch surface drop-target events for internal drags and external NSDraggingDestination - Implement child window popups (cocoa_surface_create_popup, cocoa_surface_popup_reposition) - Implement primary selection via dedicated pasteboard - Support drag images with floating borderless NSWindow - Track pointer buttons and push YASL_INPUT_RESET on focus loss - Set YASL_CAP_POPUPS and YASL_CAP_PRIMARY_SELECTION capabilities - Tests: - Add comprehensive unit test suite in tests/x11/test_tablet.c
-rw-r--r--src/cocoa/window.m519
-rw-r--r--src/x11/window.c727
-rw-r--r--tests/x11/test_tablet.c252
3 files changed, 1432 insertions, 66 deletions
diff --git a/src/cocoa/window.m b/src/cocoa/window.m
index 7b59865..78ab0bd 100644
--- a/src/cocoa/window.m
+++ b/src/cocoa/window.m
@@ -55,6 +55,10 @@ struct cocoa_surface {
id<MTLCommandQueue> software_queue;
CADisplayLink *link;
+ struct cocoa_surface *parent;
+ bool is_popup;
+ struct yasl_vector2i content_offset;
+
const struct yasl_surface_cbs *cbs;
void *ud;
uint64_t frames_skipped;
@@ -91,6 +95,7 @@ struct cocoa_surface {
double scroll_x;
uint32_t state;
uint32_t mods;
+ uint32_t buttons;
enum yasl_cursor cursor;
enum yasl_pointer_device tablet_device;
bool tablet_down;
@@ -118,8 +123,14 @@ struct cocoa_display {
struct cocoa_surface *origin;
struct cocoa_surface *carried;
struct yasl_vector2i carry_offset;
+ struct cocoa_surface *hover;
+ struct yasl_vector2i hover_pos;
struct yasl_drag_listener listener;
void *ud;
+ NSImage *image;
+ NSWindow *image_window;
+ struct yasl_vector2i image_size;
+ struct yasl_vector2i image_hotspot;
} drag;
int depth;
bool open;
@@ -135,9 +146,29 @@ static void drag_end(struct cocoa_display *d, enum yasl_drag_end how)
if (!d || !d->drag.active) return;
struct yasl_drag_listener l = d->drag.listener;
void *ud = d->drag.ud;
+ struct cocoa_surface *hover = d->drag.hover;
+ struct yasl_vector2i pos = d->drag.hover_pos;
+
d->drag.active = false;
+ d->drag.listener = (struct yasl_drag_listener){0};
+ d->drag.hover = NULL;
d->drag.origin = NULL;
d->drag.carried = NULL;
+
+ if (d->drag.image_window) {
+ [d->drag.image_window close];
+ [d->drag.image_window release];
+ d->drag.image_window = nil;
+ }
+ [d->drag.image release];
+ d->drag.image = nil;
+
+ if (hover && !hover->destroyed && hover->cbs && hover->cbs->drag) {
+ if (how == YASL_DRAG_DROPPED)
+ hover->cbs->drag(&hover->base, YASL_DRAG_DROP, pos, hover->ud);
+ else
+ hover->cbs->drag(&hover->base, YASL_DRAG_LEAVE, pos, hover->ud);
+ }
if (l.end)
l.end(ud, how);
}
@@ -159,10 +190,26 @@ static void set_state(struct cocoa_surface *w, uint32_t bit, bool on)
darwin_ctx.depth--;
}
+static void check_pointer_reset(struct cocoa_surface *w)
+{
+ if (!w || w->closed) return;
+ if (w->buttons != 0 || w->tablet_down) {
+ struct yasl_input_event ev = {
+ .kind = YASL_INPUT_RESET,
+ .surface = &w->base,
+ };
+ input_queue_push(&w->input, &ev);
+ w->buttons = 0;
+ w->tablet_down = false;
+ }
+}
+
static void push_focus(struct cocoa_surface *w, bool focused)
{
if (!w || w->closed)
return;
+ if (!focused)
+ check_pointer_reset(w);
struct yasl_input_event ev = {
.kind = YASL_INPUT_FOCUS,
.surface = &w->base,
@@ -303,6 +350,76 @@ static struct yasl_vector2i event_pos(struct cocoa_surface *w, NSEvent *ev)
};
}
+static void update_drag_hover(struct cocoa_display *d, NSPoint screenPoint)
+{
+ if (!d || !d->drag.active) return;
+
+ NSInteger excludeNum = 0;
+ if (d->drag.carried && d->drag.carried->window)
+ excludeNum = d->drag.carried->window.windowNumber;
+ else if (d->drag.image_window)
+ excludeNum = d->drag.image_window.windowNumber;
+
+ NSInteger winNum = [NSWindow windowNumberAtPoint:screenPoint belowWindowWithWindowNumber:excludeNum];
+ struct cocoa_surface *target = NULL;
+ struct yasl_vector2i pos = {0};
+
+ if (winNum > 0) {
+ NSWindow *nswin = [NSApp windowWithWindowNumber:winNum];
+ if (nswin && [nswin isKindOfClass:[YaslWindow class]]) {
+ struct cocoa_surface *candidate = ((YaslWindow *)nswin).surface;
+ if (candidate && candidate != d->drag.carried && !candidate->closed && candidate->view) {
+ NSPoint winPoint = [candidate->window convertPointFromScreen:screenPoint];
+ NSPoint viewPoint = [candidate->view convertPoint:winPoint fromView:nil];
+ if (NSPointInRect(viewPoint, candidate->view.bounds)) {
+ CGFloat scale = candidate->window.backingScaleFactor;
+ if (scale < 1.0) scale = 1.0;
+ pos.x = (int)floor(viewPoint.x * scale);
+ pos.y = (int)floor((candidate->view.bounds.size.height - viewPoint.y) * scale);
+ target = candidate;
+ }
+ }
+ }
+ if (!target) {
+ for (int i = 0; i < d->count; i++) {
+ struct cocoa_surface *s = d->windows[i];
+ if (s != d->drag.carried && !s->closed && s->window && s->window.windowNumber == winNum && s->view) {
+ NSPoint winPoint = [s->window convertPointFromScreen:screenPoint];
+ NSPoint viewPoint = [s->view convertPoint:winPoint fromView:nil];
+ if (NSPointInRect(viewPoint, s->view.bounds)) {
+ CGFloat scale = s->window.backingScaleFactor;
+ if (scale < 1.0) scale = 1.0;
+ pos.x = (int)floor(viewPoint.x * scale);
+ pos.y = (int)floor((s->view.bounds.size.height - viewPoint.y) * scale);
+ target = s;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ if (target != d->drag.hover) {
+ if (d->drag.hover) {
+ struct cocoa_surface *old = d->drag.hover;
+ struct yasl_vector2i old_pos = d->drag.hover_pos;
+ d->drag.hover = NULL;
+ if (old->cbs && old->cbs->drag && !old->destroyed)
+ old->cbs->drag(&old->base, YASL_DRAG_LEAVE, old_pos, old->ud);
+ if (!d->drag.active) return;
+ }
+ if (target && target->destroyed) target = NULL;
+ d->drag.hover = target;
+ d->drag.hover_pos = pos;
+ if (target && !target->destroyed && target->cbs && target->cbs->drag)
+ target->cbs->drag(&target->base, YASL_DRAG_OVER, pos, target->ud);
+ } else if (target && !target->destroyed) {
+ d->drag.hover_pos = pos;
+ if (target->cbs && target->cbs->drag)
+ target->cbs->drag(&target->base, YASL_DRAG_OVER, pos, target->ud);
+ }
+}
+
static void push_pointer(struct cocoa_surface *w, NSEvent *ev, enum yasl_pointer_kind kind,
int button)
{
@@ -349,6 +466,11 @@ static void push_pointer(struct cocoa_surface *w, NSEvent *ev, enum yasl_pointer
}
}
+ if (kind == YASL_PTR_PRESS && button >= 0 && button < 32)
+ w->buttons |= (1u << button);
+ else if (kind == YASL_PTR_RELEASE && button >= 0 && button < 32)
+ w->buttons &= ~(1u << button);
+
struct yasl_input_event input_ev = {
.surface = &w->base,
.kind = YASL_INPUT_MOUSE,
@@ -362,9 +484,22 @@ static void push_pointer(struct cocoa_surface *w, NSEvent *ev, enum yasl_pointer
},
};
input_queue_push(&w->input, &input_ev);
- if (kind == YASL_PTR_MOTION && darwin_ctx.drag.active && darwin_ctx.drag.carried) {
- cocoa_surface_place_under_pointer(darwin_ctx.drag.carried, darwin_ctx.drag.carry_offset);
+ if (kind == YASL_PTR_MOTION && darwin_ctx.drag.active) {
+ if (darwin_ctx.drag.carried) {
+ cocoa_surface_place_under_pointer(darwin_ctx.drag.carried, darwin_ctx.drag.carry_offset);
+ } else if (darwin_ctx.drag.image_window) {
+ CGFloat scale = darwin_ctx.drag.origin && darwin_ctx.drag.origin->window
+ ? darwin_ctx.drag.origin->window.backingScaleFactor : 1.0;
+ if (scale < 1.0) scale = 1.0;
+ NSPoint mouse = [NSEvent mouseLocation];
+ CGFloat pt_h = darwin_ctx.drag.image_size.y / scale;
+ [darwin_ctx.drag.image_window setFrameOrigin:NSMakePoint(
+ mouse.x - darwin_ctx.drag.image_hotspot.x / scale,
+ mouse.y - pt_h + darwin_ctx.drag.image_hotspot.y / scale)];
+ }
+ update_drag_hover(&darwin_ctx, [NSEvent mouseLocation]);
} else if (kind == YASL_PTR_RELEASE && darwin_ctx.drag.active) {
+ update_drag_hover(&darwin_ctx, [NSEvent mouseLocation]);
drag_end(&darwin_ctx, YASL_DRAG_DROPPED);
}
if (proximity) {
@@ -417,6 +552,18 @@ static NSCursor *cocoa_cursor(enum yasl_cursor cursor)
}
@end
+static struct yasl_vector2i dragging_pos(YaslView *view, id<NSDraggingInfo> sender)
+{
+ NSPoint p = [view convertPoint:[sender draggingLocation] fromView:nil];
+ NSWindow *win = view.window;
+ CGFloat scale = win ? win.backingScaleFactor : 1.0;
+ if (scale < 1.0) scale = 1.0;
+ return (struct yasl_vector2i){
+ .x = (int)floor(p.x * scale),
+ .y = (int)floor((view.bounds.size.height - p.y) * scale),
+ };
+}
+
@implementation YaslView
- (instancetype)initWithFrame:(NSRect)frameRect
@@ -434,8 +581,14 @@ static NSCursor *cocoa_cursor(enum yasl_cursor cursor)
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
{
NSPasteboard *pb = [sender draggingPasteboard];
- if ([pb canReadItemWithDataConformingToTypes:@[NSPasteboardTypeFileURL]])
+ if ([pb canReadItemWithDataConformingToTypes:@[NSPasteboardTypeFileURL]]) {
+ struct cocoa_surface *w = self.surface;
+ if (w && w->cbs && w->cbs->drop) {
+ struct yasl_vector2i pos = dragging_pos(self, sender);
+ w->cbs->drop(&w->base, YASL_DROP_OVER, pos, w->ud);
+ }
return NSDragOperationCopy;
+ }
return NSDragOperationNone;
}
@@ -444,33 +597,49 @@ static NSCursor *cocoa_cursor(enum yasl_cursor cursor)
return [self draggingEntered:sender];
}
+- (void)draggingExited:(id<NSDraggingInfo>)sender
+{
+ NSPasteboard *pb = [sender draggingPasteboard];
+ if ([pb canReadItemWithDataConformingToTypes:@[NSPasteboardTypeFileURL]]) {
+ struct cocoa_surface *w = self.surface;
+ if (w && w->cbs && w->cbs->drop) {
+ struct yasl_vector2i pos = dragging_pos(self, sender);
+ w->cbs->drop(&w->base, YASL_DROP_LEAVE, pos, w->ud);
+ }
+ }
+}
+
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
{
struct cocoa_surface *w = self.surface;
if (!w) return NO;
+ struct yasl_vector2i pos = dragging_pos(self, sender);
+ BOOL ok = NO;
+
NSPasteboard *pb = [sender draggingPasteboard];
NSArray<NSURL *> *urls = [pb readObjectsForClasses:@[[NSURL class]]
options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}];
- if (!urls.count)
- return NO;
-
- NSMutableString *result = [NSMutableString string];
- for (NSURL *url in urls) {
- [result appendString:url.absoluteString];
- [result appendString:@"\r\n"];
- }
- const char *utf8 = result.UTF8String;
- if (utf8 && strlen(utf8)) {
- struct yasl_input_event ev = {
- .kind = YASL_INPUT_DROP_FILES,
- .surface = &w->base,
- .drop_files = { .files = utf8, .len = strlen(utf8) },
- };
- input_queue_push(&w->input, &ev);
- cocoa_surface_schedule_redraw(w);
- return YES;
+ if (urls.count) {
+ NSMutableString *result = [NSMutableString string];
+ for (NSURL *url in urls) {
+ [result appendString:url.absoluteString];
+ [result appendString:@"\r\n"];
+ }
+ const char *utf8 = result.UTF8String;
+ if (utf8 && strlen(utf8)) {
+ struct yasl_input_event ev = {
+ .kind = YASL_INPUT_DROP_FILES,
+ .surface = &w->base,
+ .drop_files = { .files = utf8, .len = strlen(utf8) },
+ };
+ input_queue_push(&w->input, &ev);
+ cocoa_surface_schedule_redraw(w);
+ ok = YES;
+ }
}
- return NO;
+ if (w->cbs && w->cbs->drop)
+ w->cbs->drop(&w->base, YASL_DROP_LEAVE, pos, w->ud);
+ return ok;
}
- (CALayer *)makeBackingLayer
@@ -487,7 +656,7 @@ static NSCursor *cocoa_cursor(enum yasl_cursor cursor)
return layer;
}
-- (BOOL)isOpaque { return YES; }
+- (BOOL)isOpaque { return !self.surface || !self.surface->is_popup; }
- (BOOL)acceptsFirstResponder { return YES; }
- (BOOL)becomeFirstResponder { return YES; }
- (BOOL)canBecomeKeyView { return YES; }
@@ -1128,7 +1297,8 @@ void cocoa_display_close(struct cocoa_display *d)
uint32_t cocoa_display_caps(const struct cocoa_display *d)
{
- return d ? (YASL_CAP_DRAG | YASL_CAP_DRAG_CARRY | YASL_CAP_PLACE_WINDOW |
+ return d ? (YASL_CAP_POPUPS | YASL_CAP_DRAG | YASL_CAP_DRAG_CARRY |
+ YASL_CAP_PLACE_WINDOW | YASL_CAP_PRIMARY_SELECTION |
YASL_CAP_CSD | YASL_CAP_NATIVE_GRAPHICS |
YASL_CAP_FRAME_PACING | YASL_CAP_TABLET) : 0;
}
@@ -1248,8 +1418,25 @@ void cocoa_surface_destroy(struct cocoa_surface *w)
{
assert([NSThread isMainThread]);
if (!w || w->destroyed) return;
- if (darwin_ctx.drag.active && (darwin_ctx.drag.origin == w || darwin_ctx.drag.carried == w))
- drag_end(&darwin_ctx, YASL_DRAG_CANCELLED);
+ if (darwin_ctx.drag.active) {
+ if (darwin_ctx.drag.origin == w || darwin_ctx.drag.carried == w)
+ drag_end(&darwin_ctx, YASL_DRAG_CANCELLED);
+ else if (darwin_ctx.drag.hover == w)
+ darwin_ctx.drag.hover = NULL;
+ }
+ if (w->is_popup && w->parent && w->parent->window) {
+ [w->parent->window removeChildWindow:w->window];
+ w->parent = NULL;
+ }
+ for (int i = 0; i < darwin_ctx.count; i++) {
+ if (darwin_ctx.windows[i]->parent == w) {
+ if (w->window && darwin_ctx.windows[i]->window)
+ [w->window removeChildWindow:darwin_ctx.windows[i]->window];
+ darwin_ctx.windows[i]->parent = NULL;
+ }
+ }
+ w->buttons = 0;
+ w->tablet_down = false;
darwin_ctx.depth++;
bool native_closed = w->closed;
static const struct yasl_surface_cbs no_callbacks = {0};
@@ -1349,22 +1536,37 @@ bool cocoa_surface_ime_preedit_cursor(const struct cocoa_surface *w, int *begin,
return false;
}
+static NSPasteboard *pasteboard_for_selection(enum yasl_selection sel)
+{
+ switch (sel) {
+ case YASL_CLIPBOARD:
+ return [NSPasteboard generalPasteboard];
+ case YASL_PRIMARY:
+ return [NSPasteboard pasteboardWithName:@"dev.wsap.yasl.primary"];
+ default:
+ return nil;
+ }
+}
+
void cocoa_display_copy(struct cocoa_display *d, enum yasl_selection sel, const char *text)
{
- (void)d; (void)sel;
+ (void)d;
if (!text) return;
+ NSPasteboard *pb = pasteboard_for_selection(sel);
+ if (!pb) return;
NSString *s = [NSString stringWithUTF8String:text];
if (!s) return;
- NSPasteboard *pb = [NSPasteboard generalPasteboard];
[pb clearContents];
[pb setString:s forType:NSPasteboardTypeString];
}
void cocoa_display_paste(struct cocoa_display *d, enum yasl_selection sel, struct cocoa_surface *target)
{
- (void)d; (void)sel;
+ (void)d;
if (!target) return;
- NSString *s = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString];
+ NSPasteboard *pb = pasteboard_for_selection(sel);
+ if (!pb) return;
+ NSString *s = [pb stringForType:NSPasteboardTypeString];
const char *utf8 = s ? s.UTF8String : NULL;
if (utf8) {
struct yasl_input_event ev = {
@@ -1390,18 +1592,182 @@ struct yasl_geometry cocoa_surface_geometry(const struct cocoa_surface *w)
};
}
+static bool popup_spec_valid(const struct cocoa_surface *parent,
+ const struct yasl_popup_spec *spec)
+{
+ if (!parent || !spec) return false;
+ int parent_cx = parent->is_popup ? parent->content_offset.x : 0;
+ int parent_cy = parent->is_popup ? parent->content_offset.y : 0;
+ int64_t x = (int64_t)spec->anchor.x - parent_cx;
+ int64_t y = (int64_t)spec->anchor.y - parent_cy;
+ return x >= -32768 && x <= 32767 && y >= -32768 && y <= 32767 &&
+ spec->anchor.width >= 0 && spec->anchor.height >= 0 &&
+ spec->size.x > 0 && spec->size.y > 0 &&
+ spec->size.x <= 32767 && spec->size.y <= 32767 &&
+ spec->content_offset.x >= 0 && spec->content_offset.y >= 0 &&
+ spec->content_size.x >= 0 && spec->content_size.y >= 0 &&
+ (int64_t)spec->content_offset.x + spec->content_size.x <= spec->size.x &&
+ (int64_t)spec->content_offset.y + spec->content_size.y <= spec->size.y;
+}
+
+static NSRect popup_calc_frame(struct cocoa_surface *parent, const struct yasl_popup_spec *spec)
+{
+ CGFloat scale = parent->window ? parent->window.backingScaleFactor : 1.0;
+ if (scale < 1.0) scale = 1.0;
+
+ CGFloat pcx = (parent->is_popup ? parent->content_offset.x : 0) / scale;
+ CGFloat pcy = (parent->is_popup ? parent->content_offset.y : 0) / scale;
+
+ CGFloat pw = spec->size.x / scale;
+ CGFloat ph = spec->size.y / scale;
+ CGFloat ax = (spec->anchor.x / scale) - pcx;
+ CGFloat ay = (spec->anchor.y / scale) - pcy;
+ CGFloat aw = spec->anchor.width / scale;
+ CGFloat ah = spec->anchor.height / scale;
+ CGFloat ox = spec->content_offset.x / scale;
+ CGFloat oy = spec->content_offset.y / scale;
+
+ CGFloat top_left_x = spec->beside ? (ax + aw - ox) : (ax - ox);
+ CGFloat top_left_y = spec->beside ? (ay - oy) : (ay + ah - oy);
+
+ CGFloat view_h = parent->view.bounds.size.height;
+ CGFloat view_y = view_h - top_left_y;
+
+ NSPoint ptInView = NSMakePoint(top_left_x, view_y);
+ NSPoint ptInWin = [parent->view convertPoint:ptInView toView:nil];
+ NSPoint screen_tl = [parent->window convertPointToScreen:ptInWin];
+
+ NSRect frame = NSMakeRect(screen_tl.x, screen_tl.y - ph, pw, ph);
+
+ NSScreen *screen = parent->window.screen ?: [NSScreen mainScreen];
+ if (screen) {
+ NSRect visible = screen.visibleFrame;
+ if (NSMaxX(frame) > NSMaxX(visible)) {
+ if (spec->beside) {
+ CGFloat flip_tl_x = ax - pw + ox;
+ NSPoint flip_v = NSMakePoint(flip_tl_x, view_y);
+ NSPoint flip_w = [parent->view convertPoint:flip_v toView:nil];
+ NSPoint flip_s = [parent->window convertPointToScreen:flip_w];
+ frame.origin.x = flip_s.x;
+ }
+ if (NSMaxX(frame) > NSMaxX(visible))
+ frame.origin.x = NSMaxX(visible) - frame.size.width;
+ }
+ if (NSMinX(frame) < NSMinX(visible))
+ frame.origin.x = NSMinX(visible);
+
+ if (NSMinY(frame) < NSMinY(visible)) {
+ if (!spec->beside) {
+ CGFloat flip_top_y = ay - ph + oy;
+ CGFloat flip_vy = view_h - flip_top_y;
+ NSPoint flip_v = NSMakePoint(top_left_x, flip_vy);
+ NSPoint flip_w = [parent->view convertPoint:flip_v toView:nil];
+ NSPoint flip_s = [parent->window convertPointToScreen:flip_w];
+ frame.origin.y = flip_s.y - ph;
+ }
+ if (NSMinY(frame) < NSMinY(visible))
+ frame.origin.y = NSMinY(visible);
+ }
+ if (NSMaxY(frame) > NSMaxY(visible))
+ frame.origin.y = NSMaxY(visible) - frame.size.height;
+ }
+ return frame;
+}
+
struct cocoa_surface *cocoa_surface_create_popup(struct cocoa_surface *parent,
const struct yasl_popup_spec *spec)
{
- (void)parent; (void)spec;
- return NULL;
+ assert([NSThread isMainThread]);
+ if (!parent || parent->closed || !parent->window || !parent->view ||
+ !popup_spec_valid(parent, spec))
+ return NULL;
+
+ @autoreleasepool {
+ if (darwin_ctx.count >= darwin_ctx.capacity) {
+ int cap = darwin_ctx.capacity ? darwin_ctx.capacity * 2 : 16;
+ struct cocoa_surface **wins = realloc(darwin_ctx.windows, (size_t)cap * sizeof(*wins));
+ if (!wins) return NULL;
+ darwin_ctx.windows = wins;
+ darwin_ctx.capacity = cap;
+ }
+
+ struct cocoa_surface *w = calloc(1, sizeof(*w));
+ if (!w) return NULL;
+ w->base.display = parent->base.display;
+ cocoa_surface_set_callbacks(w, NULL, NULL);
+ w->clock.refresh_ns = parent->clock.refresh_ns ? parent->clock.refresh_ns : YASL_REFRESH_DEFAULT_NS;
+ w->parent = parent;
+ w->is_popup = true;
+ w->content_offset = spec->content_offset;
+
+ NSRect frame = popup_calc_frame(parent, spec);
+ NSWindowStyleMask style = NSWindowStyleMaskBorderless;
+ w->window = [[YaslWindow alloc] initWithContentRect:frame
+ styleMask:style
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ w->window.releasedWhenClosed = NO;
+ w->window.opaque = NO;
+ w->window.backgroundColor = [NSColor clearColor];
+ w->window.hasShadow = NO;
+ w->controller = [[YaslWindowController alloc] initWithWindow:w->window];
+ w->controller.surface = w;
+ w->window.delegate = w->controller;
+ w->window.acceptsMouseMovedEvents = YES;
+ w->window.colorSpace = [NSColorSpace sRGBColorSpace];
+
+ w->view = [[YaslView alloc] initWithFrame:w->window.contentView.bounds];
+ w->window.contentView = w->view;
+ w->layer = (CAMetalLayer *)w->view.layer;
+ w->layer.opaque = NO;
+ w->layer.device = parent->layer.device;
+ if (parent->software_queue) {
+ w->software_queue = [w->layer.device newCommandQueue];
+ if (!w->software_queue) {
+ w->window.delegate = nil;
+ w->controller.surface = NULL;
+ [w->view release];
+ [w->controller release];
+ [w->window release];
+ free(w);
+ return NULL;
+ }
+ w->layer.framebufferOnly = NO;
+ } else {
+ w->layer.framebufferOnly = parent->layer.framebufferOnly;
+ }
+
+ w->link = [[w->view displayLinkWithTarget:w->view
+ selector:@selector(displayLinkFired:)] retain];
+ [w->link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
+
+ w->window.surface = w;
+ w->view.surface = w;
+ darwin_ctx.windows[darwin_ctx.count++] = w;
+
+ [parent->window addChildWindow:w->window ordered:NSWindowAbove];
+ [w->window orderFront:nil];
+
+ sync_size(w, true);
+ return w;
+ }
}
bool cocoa_surface_popup_reposition(struct cocoa_surface *popup,
const struct yasl_popup_spec *spec)
{
- (void)popup; (void)spec;
- return false;
+ if (!popup || popup->closed || !popup->is_popup || !popup->parent ||
+ !popup->parent->window || !popup->parent->view ||
+ !popup_spec_valid(popup->parent, spec))
+ return false;
+
+ @autoreleasepool {
+ popup->content_offset = spec->content_offset;
+ NSRect frame = popup_calc_frame(popup->parent, spec);
+ [popup->window setFrame:frame display:YES];
+ sync_size(popup, false);
+ return true;
+ }
}
bool cocoa_surface_start_interactive_move(struct cocoa_surface *s)
@@ -1411,18 +1777,83 @@ bool cocoa_surface_start_interactive_move(struct cocoa_surface *s)
return true;
}
+static NSImage *image_from_pixels(const uint32_t *pixels, int width, int height)
+{
+ if (!pixels || width <= 0 || height <= 0)
+ return nil;
+ CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
+ if (!cs) return nil;
+ size_t bytes = (size_t)width * (size_t)height * 4;
+ CGDataProviderRef provider = CGDataProviderCreateWithCopy((const uint8_t *)pixels, bytes);
+ if (!provider) {
+ CGColorSpaceRelease(cs);
+ return nil;
+ }
+ CGImageRef cgImage = CGImageCreate((size_t)width, (size_t)height,
+ 8, 32, (size_t)width * 4, cs,
+ kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little,
+ provider, NULL, NO, kCGRenderingIntentDefault);
+ CGDataProviderRelease(provider);
+ CGColorSpaceRelease(cs);
+ if (!cgImage)
+ return nil;
+ NSImage *image = [[NSImage alloc] initWithCGImage:cgImage size:NSMakeSize(width, height)];
+ CGImageRelease(cgImage);
+ return image;
+}
+
bool cocoa_display_drag_start(struct cocoa_display *d, struct cocoa_surface *origin,
const struct yasl_drag_image *image,
const struct yasl_drag_listener *l, void *ud)
{
- (void)image;
if (!d || !origin || d->drag.active || !l)
return false;
d->drag.active = true;
d->drag.origin = origin;
d->drag.carried = NULL;
+ d->drag.hover = NULL;
+ d->drag.hover_pos = (struct yasl_vector2i){0};
d->drag.listener = *l;
d->drag.ud = ud;
+ [d->drag.image release];
+ d->drag.image = nil;
+ if (d->drag.image_window) {
+ [d->drag.image_window close];
+ [d->drag.image_window release];
+ d->drag.image_window = nil;
+ }
+ if (image && image->pixels && image->size.x > 0 && image->size.y > 0) {
+ d->drag.image = image_from_pixels(image->pixels, image->size.x, image->size.y);
+ d->drag.image_size = image->size;
+ d->drag.image_hotspot = image->hotspot;
+ if (d->drag.image) {
+ CGFloat scale = origin->window ? origin->window.backingScaleFactor : 1.0;
+ if (scale < 1.0) scale = 1.0;
+ CGFloat pt_w = image->size.x / scale;
+ CGFloat pt_h = image->size.y / scale;
+ NSWindow *win = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, pt_w, pt_h)
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ win.releasedWhenClosed = NO;
+ win.opaque = NO;
+ win.backgroundColor = [NSColor clearColor];
+ win.ignoresMouseEvents = YES;
+ win.level = NSFloatingWindowLevel;
+ win.hasShadow = YES;
+ NSImageView *iv = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, pt_w, pt_h)];
+ iv.image = d->drag.image;
+ iv.imageScaling = NSImageScaleAxesIndependently;
+ win.contentView = iv;
+ [iv release];
+ d->drag.image_window = win;
+ NSPoint mouse = [NSEvent mouseLocation];
+ [win setFrameOrigin:NSMakePoint(mouse.x - image->hotspot.x / scale,
+ mouse.y - pt_h + image->hotspot.y / scale)];
+ [win orderFront:nil];
+ }
+ }
+ update_drag_hover(d, [NSEvent mouseLocation]);
return true;
}
@@ -1435,6 +1866,11 @@ bool cocoa_surface_drag_carry(struct cocoa_surface *w, struct yasl_vector2i offs
{
if (!w || w->closed || !darwin_ctx.drag.active)
return false;
+ if (darwin_ctx.drag.image_window) {
+ [darwin_ctx.drag.image_window close];
+ [darwin_ctx.drag.image_window release];
+ darwin_ctx.drag.image_window = nil;
+ }
darwin_ctx.drag.carried = w;
darwin_ctx.drag.carry_offset = offset;
return cocoa_surface_place_under_pointer(w, offset);
@@ -1475,7 +1911,18 @@ void cocoa_surface_set_callbacks(struct cocoa_surface *w,
bool cocoa_surface_input_poll(struct cocoa_surface *w, struct yasl_input_event *ev)
{
- return w && ev && input_queue_pop(&w->input, ev);
+ if (!w || !ev || !input_queue_pop(&w->input, ev))
+ return false;
+ if (ev->kind == YASL_INPUT_RESET) {
+ w->buttons = 0;
+ w->tablet_down = false;
+ w->mods = 0;
+ if (w->preedit) {
+ free(w->preedit);
+ w->preedit = NULL;
+ }
+ }
+ return true;
}
uint32_t cocoa_surface_state(const struct cocoa_surface *w)
diff --git a/src/x11/window.c b/src/x11/window.c
index 5e3dba9..5e1b612 100644
--- a/src/x11/window.c
+++ b/src/x11/window.c
@@ -48,6 +48,9 @@ struct x11_surface {
struct yasl_frame_clock clock;
uint32_t state;
bool want_draw, drawing, presented, destroyed;
+ struct x11_surface *parent;
+ bool is_popup;
+ struct yasl_vector2i content_offset;
};
struct paste_request {
@@ -80,6 +83,8 @@ struct x11_display {
Window selection_window;
Atom protocols, delete_window, utf8, clipboard, targets, paste_property;
Atom net_name, net_state, fullscreen, maximized_h, maximized_v;
+ Atom xdnd_aware, xdnd_enter, xdnd_position, xdnd_status, xdnd_leave, xdnd_drop, xdnd_finished;
+ Atom xdnd_selection, xdnd_action_copy, xdnd_type_list, uri_list;
char *selection[2];
struct paste_request *pastes;
uint32_t mods;
@@ -96,7 +101,27 @@ struct x11_display {
struct yasl_vector2i carry_offset;
struct yasl_drag_listener listener;
void *ud;
+ struct x11_surface *hover;
+ struct yasl_vector2i hover_pos;
+ Window icon_window;
+ Pixmap icon_pixmap;
+ struct yasl_vector2i hotspot;
} drag;
+ struct {
+ Window source;
+ int version;
+ bool accepts;
+ bool has_uri_list;
+ struct x11_surface *surface;
+ struct yasl_vector2i pos;
+ Time time;
+ } xdnd;
+ struct {
+ bool active;
+ Window source;
+ struct x11_surface *surface;
+ Time time;
+ } pending_drop;
};
static struct x11_display *display_of(const struct yasl_surface *s)
@@ -107,6 +132,7 @@ static struct x11_display *display_of(const struct yasl_surface *s)
static bool place(struct yasl_surface *base, struct yasl_vector2i at);
static void drag_end(struct x11_display *d, enum yasl_drag_end how);
static void drag_cancel(struct yasl_display *base);
+static void schedule_redraw(struct yasl_surface *base);
static void leave(struct x11_display *d)
{
@@ -126,6 +152,9 @@ static void push(struct x11_surface *s, struct yasl_input_event event,
if (event.kind == YASL_INPUT_TEXT) {
event.text.text = text;
event.text.len = len;
+ } else if (event.kind == YASL_INPUT_DROP_FILES) {
+ event.drop_files.files = text;
+ event.drop_files.len = len;
} else {
event.paste.text = text;
event.paste.len = len;
@@ -135,6 +164,125 @@ static void push(struct x11_surface *s, struct yasl_input_event event,
input_queue_push(&s->input, &event);
}
+static unsigned long component(uint32_t color, unsigned long mask)
+{
+ if (unlikely(!mask)) return 0;
+ unsigned shift = (unsigned)__builtin_ctzl(mask);
+ unsigned long m = mask >> shift;
+ return (((unsigned long)color * m + 127) / 255) << shift;
+}
+
+static void drag_set_hover(struct x11_display *d, struct x11_surface *s, struct yasl_vector2i pos)
+{
+ if (!d->drag.active) return;
+ if (d->drag.hover && d->drag.hover != s) {
+ struct x11_surface *old = d->drag.hover;
+ struct yasl_vector2i old_pos = d->drag.hover_pos;
+ d->drag.hover = NULL;
+ if (!old->destroyed && old->cbs && old->cbs->drag)
+ old->cbs->drag(&old->base, YASL_DRAG_LEAVE, old_pos, old->ud);
+ if (!d->drag.active) return;
+ }
+ if (s && s->destroyed) return;
+ d->drag.hover = s;
+ d->drag.hover_pos = pos;
+ if (s && s->cbs && s->cbs->drag)
+ s->cbs->drag(&s->base, YASL_DRAG_OVER, pos, s->ud);
+}
+
+static void drag_leave_hover(struct x11_display *d, struct x11_surface *s, struct yasl_vector2i pos)
+{
+ if (!d->drag.active || d->drag.hover != s) return;
+ d->drag.hover = NULL;
+ if (s && !s->destroyed && s->cbs && s->cbs->drag)
+ s->cbs->drag(&s->base, YASL_DRAG_LEAVE, pos, s->ud);
+}
+
+static void drag_drop_hover(struct x11_display *d)
+{
+ if (!d->drag.active) return;
+ drag_end(d, YASL_DRAG_DROPPED);
+}
+
+static struct x11_surface *surface_at_root_pos(struct x11_display *d, int rx, int ry, struct yasl_vector2i *out_pos)
+{
+ if (!d->native) return NULL;
+ Window child = None;
+ int dx, dy;
+ if (!XTranslateCoordinates(d->native, RootWindow(d->native, d->screen),
+ RootWindow(d->native, d->screen),
+ rx, ry, &dx, &dy, &child) || !child)
+ return NULL;
+
+ Window ignore_icon = d->drag.icon_window;
+ Window ignore_carried = d->drag.carried ? d->drag.carried->window : None;
+
+ if (child == ignore_icon || child == ignore_carried) {
+ Window root, parent, *children = NULL;
+ unsigned int nchildren = 0;
+ child = None;
+ if (XQueryTree(d->native, RootWindow(d->native, d->screen),
+ &root, &parent, &children, &nchildren) && children) {
+ for (int i = (int)nchildren - 1; i >= 0; i--) {
+ Window w = children[i];
+ if (w == ignore_icon || w == ignore_carried) continue;
+ for (struct x11_surface *s = d->surfaces; s; s = s->next) {
+ int tx = 0, ty = 0;
+ Window sub = None;
+ if (XTranslateCoordinates(d->native, RootWindow(d->native, d->screen),
+ s->window, rx, ry, &tx, &ty, &sub)) {
+ if (tx >= 0 && ty >= 0 && tx < s->size.x && ty < s->size.y) {
+ if (s->window == w) {
+ child = w;
+ break;
+ }
+ Window cur = s->window;
+ while (cur && cur != RootWindow(d->native, d->screen)) {
+ if (cur == w) { child = w; break; }
+ Window cr, cp, *cc = NULL;
+ unsigned int ncc = 0;
+ if (!XQueryTree(d->native, cur, &cr, &cp, &cc, &ncc)) break;
+ if (cc) XFree(cc);
+ cur = cp;
+ }
+ if (child) break;
+ }
+ }
+ }
+ if (child) break;
+ }
+ XFree(children);
+ }
+ }
+
+ for (struct x11_surface *s = d->surfaces; s; s = s->next) {
+ int tx = 0, ty = 0;
+ Window sub = None;
+ if (XTranslateCoordinates(d->native, RootWindow(d->native, d->screen),
+ s->window, rx, ry, &tx, &ty, &sub)) {
+ if (tx >= 0 && ty >= 0 && tx < s->size.x && ty < s->size.y) {
+ if (s->window == child) {
+ if (out_pos) *out_pos = (struct yasl_vector2i){tx, ty};
+ return s;
+ }
+ Window cur = s->window;
+ while (cur && cur != RootWindow(d->native, d->screen)) {
+ if (cur == child) {
+ if (out_pos) *out_pos = (struct yasl_vector2i){tx, ty};
+ return s;
+ }
+ Window cr, cp, *cc = NULL;
+ unsigned int ncc = 0;
+ if (!XQueryTree(d->native, cur, &cr, &cp, &cc, &ncc)) break;
+ if (cc) XFree(cc);
+ cur = cp;
+ }
+ }
+ }
+ }
+ return NULL;
+}
+
static uint32_t modifiers(unsigned state)
{
return ((state & ShiftMask) ? YASL_MOD_SHIFT : 0) |
@@ -178,6 +326,7 @@ static struct x11_pointer *pointer_for(struct x11_display *d, int id)
{
for (struct x11_pointer *p = d->pointers; p; p = p->next)
if (p->id == id) return p;
+ if (!d->native) return NULL;
int count = 0;
XIDeviceInfo *info = XIQueryDevice(d->native, id, &count);
if (!info) return NULL;
@@ -286,6 +435,8 @@ static void xi_event(struct x11_display *d, XGenericEventCookie *cookie)
(((XIHierarchyEvent *)cookie->data)->flags &
(XISlaveRemoved | XIDeviceDisabled | XISlaveDetached | XIMasterRemoved))) {
/* Removal/disable may interrupt a drag before a release arrives. */
+ if (d->drag.active)
+ drag_end(d, YASL_DRAG_CANCELLED);
for (struct x11_surface *s = d->surfaces; s; s = s->next)
push(s, (struct yasl_input_event){.kind = YASL_INPUT_RESET}, NULL, 0);
}
@@ -296,13 +447,37 @@ static void xi_event(struct x11_display *d, XGenericEventCookie *cookie)
cookie->evtype != XI_ButtonRelease) return;
Window window = crossing ? ((XIEnterEvent *)cookie->data)->event :
((XIDeviceEvent *)cookie->data)->event;
- if (cookie->evtype == XI_Motion && d->drag.active && d->drag.carried)
- place(&d->drag.carried->base, d->drag.carry_offset);
- else if (cookie->evtype == XI_ButtonRelease && d->drag.active)
- drag_end(d, YASL_DRAG_DROPPED);
struct x11_surface *s = d->surfaces;
while (s && s->window != window) s = s->next;
- if (!s) return;
+ if (cookie->evtype == XI_Motion && d->drag.active) {
+ const XIDeviceEvent *mev = cookie->data;
+ if (d->drag.carried)
+ place(&d->drag.carried->base, d->drag.carry_offset);
+ else if (d->drag.icon_window && d->native)
+ XMoveWindow(d->native, d->drag.icon_window,
+ (int)round(mev->root_x) - d->drag.hotspot.x,
+ (int)round(mev->root_y) - d->drag.hotspot.y);
+ struct yasl_vector2i hpos = {yasl_scale_to_device(mev->event_x, YASL_SCALE_UNIT),
+ yasl_scale_to_device(mev->event_y, YASL_SCALE_UNIT)};
+ struct x11_surface *hsurf = d->native ?
+ surface_at_root_pos(d, (int)round(mev->root_x), (int)round(mev->root_y), &hpos) : s;
+ drag_set_hover(d, hsurf, hpos);
+ } else if (cookie->evtype == XI_Enter && d->drag.active) {
+ const XIEnterEvent *eev = cookie->data;
+ struct yasl_vector2i hpos = {yasl_scale_to_device(eev->event_x, YASL_SCALE_UNIT),
+ yasl_scale_to_device(eev->event_y, YASL_SCALE_UNIT)};
+ struct x11_surface *hsurf = d->native ?
+ surface_at_root_pos(d, (int)round(eev->root_x), (int)round(eev->root_y), &hpos) : s;
+ drag_set_hover(d, hsurf, hpos);
+ } else if (cookie->evtype == XI_Leave && d->drag.active) {
+ const XIEnterEvent *lev = cookie->data;
+ struct yasl_vector2i lpos = {yasl_scale_to_device(lev->event_x, YASL_SCALE_UNIT),
+ yasl_scale_to_device(lev->event_y, YASL_SCALE_UNIT)};
+ drag_leave_hover(d, s, lpos);
+ } else if (cookie->evtype == XI_ButtonRelease && d->drag.active) {
+ drag_drop_hover(d);
+ }
+ if (!s || s->destroyed) return;
if (!crossing) xi_pointer_event(s, cookie->data);
else {
const XIEnterEvent *ev = cookie->data;
@@ -325,6 +500,11 @@ static bool resize(struct x11_surface *s, struct yasl_vector2i size)
return false;
if (s->image && size.x == s->size.x && size.y == s->size.y) return true;
struct x11_display *d = display_of(&s->base);
+ if (!d->native) {
+ s->size = size;
+ s->want_draw = true;
+ return true;
+ }
uint64_t start = yasl_loop_now_ns();
XImage *image = XCreateImage(d->native, DefaultVisual(d->native, d->screen),
(unsigned)DefaultDepth(d->native, d->screen), ZPixmap, 0, NULL,
@@ -373,8 +553,18 @@ static void surface_destroy(struct yasl_surface *base)
struct x11_surface *s = (struct x11_surface *)base;
if (s->destroyed) return;
struct x11_display *d = display_of(base);
- if (d->drag.active && (d->drag.origin == s || d->drag.carried == s))
- drag_end(d, YASL_DRAG_CANCELLED);
+ if (d->drag.active) {
+ if (d->drag.origin == s || d->drag.carried == s)
+ drag_end(d, YASL_DRAG_CANCELLED);
+ else if (d->drag.hover == s)
+ d->drag.hover = NULL;
+ }
+ if (d->xdnd.surface == s)
+ d->xdnd.surface = NULL;
+ if (d->pending_drop.surface == s) d->pending_drop.surface = NULL;
+ for (struct x11_surface *p = d->surfaces; p; p = p->next) {
+ if (p->parent == s) p->parent = NULL;
+ }
s->destroyed = true;
for (struct x11_surface **p = &d->surfaces; *p; p = &(*p)->next) {
if (*p == s) { *p = s->next; break; }
@@ -383,8 +573,10 @@ static void surface_destroy(struct yasl_surface *base)
if (s->ic) XDestroyIC(s->ic);
if (s->image) XDestroyImage(s->image);
free(s->pixels);
- if (s->gc) XFreeGC(d->native, s->gc);
- if (s->window) XDestroyWindow(d->native, s->window);
+ if (d->native) {
+ if (s->gc) XFreeGC(d->native, s->gc);
+ if (s->window) XDestroyWindow(d->native, s->window);
+ }
input_queue_clear(&s->input);
if (d->depth) {
s->dead_next = d->dead;
@@ -442,7 +634,7 @@ static struct yasl_surface *create_toplevel(struct yasl_display *base,
XSelectInput(d->native, s->window, ExposureMask | StructureNotifyMask |
FocusChangeMask | KeyPressMask | KeyReleaseMask | PropertyChangeMask |
(d->xi2 ? 0 : ButtonPressMask | ButtonReleaseMask |
- PointerMotionMask | LeaveWindowMask));
+ PointerMotionMask | LeaveWindowMask | EnterWindowMask));
if (d->xi2) {
unsigned char bits[XIMaskLen(XI_LASTEVENT)] = {0};
XISetMask(bits, XI_Motion);
@@ -456,6 +648,9 @@ static struct yasl_surface *create_toplevel(struct yasl_display *base,
XSetWMProtocols(d->native, s->window, &d->delete_window, 1);
XClassHint hint = { .res_name = d->app_id, .res_class = d->app_id };
XSetClassHint(d->native, s->window, &hint);
+ Atom xdnd_version = 5;
+ XChangeProperty(d->native, s->window, d->xdnd_aware, XA_ATOM, 32,
+ PropModeReplace, (unsigned char *)&xdnd_version, 1);
if (d->im) {
s->ic = XCreateIC(d->im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
XNClientWindow, s->window, XNFocusWindow, s->window, NULL);
@@ -473,6 +668,161 @@ static struct yasl_surface *create_toplevel(struct yasl_display *base,
return &s->base;
}
+static bool popup_spec_valid(const struct x11_surface *parent,
+ const struct yasl_popup_spec *spec)
+{
+ if (!parent || !spec) return false;
+ int parent_cx = parent->is_popup ? parent->content_offset.x : 0;
+ int parent_cy = parent->is_popup ? parent->content_offset.y : 0;
+ int64_t x = (int64_t)spec->anchor.x - parent_cx;
+ int64_t y = (int64_t)spec->anchor.y - parent_cy;
+ return x >= -32768 && x <= 32767 && y >= -32768 && y <= 32767 &&
+ spec->anchor.width >= 0 && spec->anchor.height >= 0 &&
+ spec->size.x > 0 && spec->size.y > 0 &&
+ spec->size.x <= 32767 && spec->size.y <= 32767 &&
+ spec->content_offset.x >= 0 && spec->content_offset.y >= 0 &&
+ spec->content_size.x >= 0 && spec->content_size.y >= 0 &&
+ (int64_t)spec->content_offset.x + spec->content_size.x <= spec->size.x &&
+ (int64_t)spec->content_offset.y + spec->content_size.y <= spec->size.y;
+}
+
+static void popup_calculate_position(struct x11_display *d,
+ struct x11_surface *parent,
+ const struct yasl_popup_spec *spec,
+ int *out_x, int *out_y)
+{
+ int parent_rx = 0, parent_ry = 0;
+ if (d->native) {
+ Window child;
+ XTranslateCoordinates(d->native, parent->window,
+ RootWindow(d->native, d->screen),
+ 0, 0, &parent_rx, &parent_ry, &child);
+ }
+ int parent_cx = parent->is_popup ? parent->content_offset.x : 0;
+ int parent_cy = parent->is_popup ? parent->content_offset.y : 0;
+
+ int anchor_x = parent_rx + (spec->anchor.x - parent_cx);
+ int anchor_y = parent_ry + (spec->anchor.y - parent_cy);
+
+ int root_x, root_y;
+ if (spec->beside) {
+ root_x = anchor_x + spec->anchor.width - spec->content_offset.x;
+ root_y = anchor_y - spec->content_offset.y;
+ } else {
+ root_x = anchor_x - spec->content_offset.x;
+ root_y = anchor_y + spec->anchor.height - spec->content_offset.y;
+ }
+
+ int screen_w = d->native ? DisplayWidth(d->native, d->screen) : 1920;
+ int screen_h = d->native ? DisplayHeight(d->native, d->screen) : 1080;
+
+ if (spec->beside) {
+ if (root_x + spec->size.x > screen_w) {
+ int flipped_x = anchor_x - spec->size.x + spec->content_offset.x;
+ if (flipped_x >= 0) root_x = flipped_x;
+ }
+ } else {
+ if (root_y + spec->size.y > screen_h) {
+ int flipped_y = anchor_y - spec->size.y + spec->content_offset.y;
+ if (flipped_y >= 0) root_y = flipped_y;
+ }
+ }
+
+ if (root_x + spec->size.x > screen_w) root_x = screen_w - spec->size.x;
+ if (root_x < 0) root_x = 0;
+ if (root_y + spec->size.y > screen_h) root_y = screen_h - spec->size.y;
+ if (root_y < 0) root_y = 0;
+
+ *out_x = root_x;
+ *out_y = root_y;
+}
+
+static struct yasl_surface *surface_create_popup(struct yasl_surface *parent_base,
+ const struct yasl_popup_spec *spec)
+{
+ struct x11_surface *parent = (struct x11_surface *)parent_base;
+ if (!parent || parent->destroyed || !popup_spec_valid(parent, spec))
+ return NULL;
+ struct x11_display *d = display_of(parent_base);
+ struct x11_surface *s = calloc(1, sizeof(*s));
+ if (!s) return NULL;
+ s->base.display = parent_base->display;
+ s->parent = parent;
+ s->is_popup = true;
+ s->content_offset = spec->content_offset;
+ s->clock.refresh_ns = parent->clock.refresh_ns;
+ if (!resize(s, spec->size)) { free(s); return NULL; }
+
+ int root_x = 0, root_y = 0;
+ popup_calculate_position(d, parent, spec, &root_x, &root_y);
+
+ if (d->native) {
+ XSetWindowAttributes attrs = {
+ .override_redirect = True,
+ .save_under = True,
+ };
+ s->window = XCreateWindow(d->native, RootWindow(d->native, d->screen),
+ root_x, root_y, (unsigned)spec->size.x, (unsigned)spec->size.y,
+ 0, CopyFromParent, InputOutput, CopyFromParent,
+ CWOverrideRedirect | CWSaveUnder, &attrs);
+ if (!s->window) {
+ surface_destroy(&s->base);
+ return NULL;
+ }
+ s->gc = XCreateGC(d->native, s->window, 0, NULL);
+ XSelectInput(d->native, s->window, ExposureMask | StructureNotifyMask |
+ FocusChangeMask | KeyPressMask | KeyReleaseMask | PropertyChangeMask |
+ (d->xi2 ? 0 : ButtonPressMask | ButtonReleaseMask |
+ PointerMotionMask | LeaveWindowMask | EnterWindowMask));
+ if (d->xi2) {
+ unsigned char bits[XIMaskLen(XI_LASTEVENT)] = {0};
+ XISetMask(bits, XI_Motion);
+ XISetMask(bits, XI_ButtonPress);
+ XISetMask(bits, XI_ButtonRelease);
+ XISetMask(bits, XI_Enter);
+ XISetMask(bits, XI_Leave);
+ XIEventMask mask = {XIAllMasterDevices, sizeof(bits), bits};
+ XISelectEvents(d->native, s->window, &mask, 1);
+ }
+ Atom xdnd_version = 5;
+ XChangeProperty(d->native, s->window, d->xdnd_aware, XA_ATOM, 32,
+ PropModeReplace, (unsigned char *)&xdnd_version, 1);
+ if (d->im) {
+ s->ic = XCreateIC(d->im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
+ XNClientWindow, s->window, XNFocusWindow, s->window, NULL);
+ }
+ XMapRaised(d->native, s->window);
+ XFlush(d->native);
+ }
+ s->next = d->surfaces;
+ d->surfaces = s;
+ return &s->base;
+}
+
+static bool surface_popup_reposition(struct yasl_surface *popup_base,
+ const struct yasl_popup_spec *spec)
+{
+ struct x11_surface *s = (struct x11_surface *)popup_base;
+ if (!s || s->destroyed || !s->is_popup || !s->parent || s->parent->destroyed ||
+ !popup_spec_valid(s->parent, spec))
+ return false;
+ struct x11_display *d = display_of(popup_base);
+ if (spec->size.x != s->size.x || spec->size.y != s->size.y) {
+ if (!resize(s, spec->size))
+ return false;
+ }
+ s->content_offset = spec->content_offset;
+ int root_x = 0, root_y = 0;
+ popup_calculate_position(d, s->parent, spec, &root_x, &root_y);
+ if (d->native && s->window) {
+ XMoveResizeWindow(d->native, s->window, root_x, root_y,
+ (unsigned)spec->size.x, (unsigned)spec->size.y);
+ XFlush(d->native);
+ }
+ schedule_redraw(&s->base);
+ return true;
+}
+
static void update_state(struct x11_surface *s)
{
struct x11_display *d = display_of(&s->base);
@@ -526,6 +876,92 @@ static void selection_request(struct x11_display *d, XSelectionRequestEvent *req
XSendEvent(d->native, request->requestor, False, 0, &reply);
}
+static char *parse_file_uris(const char *data, size_t len, size_t *out_len)
+{
+ if (!data || !len) {
+ if (out_len) *out_len = 0;
+ return NULL;
+ }
+ char *out = malloc(len * 2 + 2);
+ if (!out) {
+ if (out_len) *out_len = 0;
+ return NULL;
+ }
+ size_t out_pos = 0;
+ size_t i = 0;
+ while (i < len) {
+ while (i < len && (data[i] == '\r' || data[i] == '\n')) i++;
+ if (i >= len) break;
+ size_t start = i;
+ while (i < len && data[i] != '\r' && data[i] != '\n') i++;
+ size_t end = i;
+ while (end > start && (data[end - 1] == ' ' || data[end - 1] == '\t' || data[end - 1] == '\r'))
+ end--;
+ size_t line_len = end - start;
+ if (line_len >= 7 && !strncmp(data + start, "file://", 7)) {
+ memcpy(out + out_pos, data + start, line_len);
+ out_pos += line_len;
+ out[out_pos++] = '\r';
+ out[out_pos++] = '\n';
+ }
+ }
+ if (!out_pos) {
+ free(out);
+ if (out_len) *out_len = 0;
+ return NULL;
+ }
+ out[out_pos] = '\0';
+ if (out_len) *out_len = out_pos;
+ return out;
+}
+
+static void xdnd_selection_notify(struct x11_display *d, XSelectionEvent *event)
+{
+ if (!d->pending_drop.active) return;
+ Window source = d->pending_drop.source;
+ struct x11_surface *s = d->pending_drop.surface;
+ d->pending_drop.active = false;
+ d->pending_drop.source = None;
+ d->pending_drop.surface = NULL;
+
+ bool success = false;
+ if (s && !s->destroyed && event->property != None) {
+ Atom actual;
+ int format;
+ unsigned long count, left;
+ unsigned char *data = NULL;
+ if (XGetWindowProperty(d->native, s->window, event->property,
+ 0, TEXT_LIMIT / 4, True, AnyPropertyType,
+ &actual, &format, &count, &left, &data) == Success && data && count > 0) {
+ size_t out_len = 0;
+ char *parsed = parse_file_uris((const char *)data, count, &out_len);
+ if (parsed && out_len > 0) {
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_DROP_FILES}, parsed, out_len);
+ free(parsed);
+ success = true;
+ }
+ }
+ if (data) XFree(data);
+ }
+ if (source) {
+ XEvent finished = {0};
+ finished.xclient = (XClientMessageEvent){
+ .type = ClientMessage,
+ .display = d->native,
+ .window = source,
+ .message_type = d->xdnd_finished,
+ .format = 32,
+ .data.l = {
+ s ? s->window : None,
+ success ? 1 : 0,
+ success ? d->xdnd_action_copy : None,
+ },
+ };
+ XSendEvent(d->native, source, False, NoEventMask, &finished);
+ XFlush(d->native);
+ }
+}
+
static void selection_notify(struct x11_display *d, XSelectionEvent *event)
{
struct paste_request **p = &d->pastes;
@@ -593,6 +1029,119 @@ static void key_event(struct x11_surface *s, XKeyEvent *key, bool repeat)
if (text != local) free(text);
}
+static void handle_client_message(struct x11_display *d, struct x11_surface *s,
+ const XClientMessageEvent *ev)
+{
+ if (ev->message_type == d->protocols &&
+ (Atom)ev->data.l[0] == d->delete_window && s->cbs && s->cbs->close) {
+ s->cbs->close(&s->base, s->ud);
+ } else if (ev->message_type == d->xdnd_enter) {
+ Window source = (Window)ev->data.l[0];
+ int version = (int)((unsigned long)ev->data.l[1] >> 24);
+ bool more_types = (ev->data.l[1] & 1) != 0;
+ bool has_uri = false;
+ if (more_types && d->native) {
+ Atom actual;
+ int format;
+ unsigned long count, left;
+ unsigned char *prop_data = NULL;
+ if (XGetWindowProperty(d->native, source, d->xdnd_type_list,
+ 0, 64, False, XA_ATOM, &actual, &format, &count, &left, &prop_data) == Success &&
+ actual == XA_ATOM && format == 32 && prop_data) {
+ Atom *types = (Atom *)prop_data;
+ for (unsigned long i = 0; i < count; i++) {
+ if (types[i] == d->uri_list) { has_uri = true; break; }
+ }
+ }
+ if (prop_data) XFree(prop_data);
+ } else {
+ for (int i = 2; i <= 4; i++) {
+ if ((Atom)ev->data.l[i] == d->uri_list) { has_uri = true; break; }
+ }
+ }
+ d->xdnd.source = source;
+ d->xdnd.version = version;
+ d->xdnd.has_uri_list = has_uri;
+ d->xdnd.accepts = has_uri;
+ } else if (ev->message_type == d->xdnd_position) {
+ Window source = (Window)ev->data.l[0];
+ int rx = (int)(int16_t)((ev->data.l[2] >> 16) & 0xFFFF);
+ int ry = (int)(int16_t)(ev->data.l[2] & 0xFFFF);
+ Time t = (Time)ev->data.l[3];
+ int wx = rx, wy = ry;
+ if (d->native) {
+ Window child;
+ XTranslateCoordinates(d->native, RootWindow(d->native, d->screen), s->window,
+ rx, ry, &wx, &wy, &child);
+ }
+ struct yasl_vector2i pos = {wx, wy};
+ if (d->xdnd.surface && d->xdnd.surface != s) {
+ if (d->xdnd.accepts && d->xdnd.surface->cbs && d->xdnd.surface->cbs->drop)
+ d->xdnd.surface->cbs->drop(&d->xdnd.surface->base, YASL_DROP_LEAVE, d->xdnd.pos, d->xdnd.surface->ud);
+ }
+ d->xdnd.surface = s;
+ d->xdnd.pos = pos;
+ d->xdnd.time = t;
+ if (d->xdnd.accepts && s->cbs && s->cbs->drop)
+ s->cbs->drop(&s->base, YASL_DROP_OVER, pos, s->ud);
+
+ if (d->native) {
+ XEvent reply = {0};
+ reply.xclient = (XClientMessageEvent){
+ .type = ClientMessage,
+ .display = d->native,
+ .window = source,
+ .message_type = d->xdnd_status,
+ .format = 32,
+ .data.l = {
+ s->window,
+ d->xdnd.accepts ? 1 : 0,
+ 0,
+ 0,
+ d->xdnd.accepts ? d->xdnd_action_copy : None,
+ },
+ };
+ XSendEvent(d->native, source, False, NoEventMask, &reply);
+ }
+ } else if (ev->message_type == d->xdnd_leave) {
+ if (d->xdnd.surface == s) {
+ if (d->xdnd.accepts && s->cbs && s->cbs->drop)
+ s->cbs->drop(&s->base, YASL_DROP_LEAVE, d->xdnd.pos, s->ud);
+ }
+ d->xdnd = (typeof(d->xdnd)){0};
+ } else if (ev->message_type == d->xdnd_drop) {
+ Window source = (Window)ev->data.l[0];
+ Time drop_time = (Time)ev->data.l[2];
+ bool was_accepting = d->xdnd.accepts && d->xdnd.has_uri_list && (d->xdnd.source == source);
+ if (d->xdnd.surface == s) {
+ if (was_accepting && s->cbs && s->cbs->drop)
+ s->cbs->drop(&s->base, YASL_DROP_LEAVE, d->xdnd.pos, s->ud);
+ }
+ if (was_accepting) {
+ d->pending_drop.active = true;
+ d->pending_drop.source = source;
+ d->pending_drop.surface = s;
+ d->pending_drop.time = drop_time;
+ if (d->native) {
+ XConvertSelection(d->native, d->xdnd_selection, d->uri_list,
+ d->paste_property, s->window, drop_time);
+ }
+ } else if (d->native) {
+ XEvent reply = {0};
+ reply.xclient = (XClientMessageEvent){
+ .type = ClientMessage,
+ .display = d->native,
+ .window = source,
+ .message_type = d->xdnd_finished,
+ .format = 32,
+ .data.l = { s->window, 0, None },
+ };
+ XSendEvent(d->native, source, False, NoEventMask, &reply);
+ }
+ d->xdnd = (typeof(d->xdnd)){0};
+ }
+}
+
static void dispatch(struct yasl_display *base)
{
struct x11_display *d = (struct x11_display *)base;
@@ -612,6 +1161,10 @@ static void dispatch(struct yasl_display *base)
continue;
}
if (event.type == SelectionNotify) {
+ if (event.xselection.selection == d->xdnd_selection) {
+ xdnd_selection_notify(d, &event.xselection);
+ continue;
+ }
selection_notify(d, &event.xselection);
continue;
}
@@ -628,9 +1181,7 @@ static void dispatch(struct yasl_display *base)
break;
}
case ClientMessage:
- if (event.xclient.message_type == d->protocols &&
- (Atom)event.xclient.data.l[0] == d->delete_window && s->cbs && s->cbs->close)
- s->cbs->close(&s->base, s->ud);
+ handle_client_message(d, s, &event.xclient);
break;
case FocusIn: case FocusOut: {
bool focused = event.type == FocusIn;
@@ -659,19 +1210,41 @@ static void dispatch(struct yasl_display *base)
key_event(s, &event.xkey, repeat);
break;
}
- case MotionNotify: case LeaveNotify: case ButtonPress: case ButtonRelease: {
+ case MotionNotify: case LeaveNotify: case EnterNotify: case ButtonPress: case ButtonRelease: {
struct yasl_mouse_event mouse = {.device = YASL_PTR_DEV_MOUSE};
unsigned state;
if (event.type == MotionNotify) {
mouse.pos = (struct yasl_vector2i){event.xmotion.x, event.xmotion.y};
mouse.kind = YASL_PTR_MOTION;
state = event.xmotion.state;
- if (d->drag.active && d->drag.carried)
- place(&d->drag.carried->base, d->drag.carry_offset);
+ if (d->drag.active) {
+ if (d->drag.carried)
+ place(&d->drag.carried->base, d->drag.carry_offset);
+ else if (d->drag.icon_window && d->native)
+ XMoveWindow(d->native, d->drag.icon_window,
+ event.xmotion.x_root - d->drag.hotspot.x,
+ event.xmotion.y_root - d->drag.hotspot.y);
+ struct yasl_vector2i hpos = mouse.pos;
+ struct x11_surface *hsurf = d->native ?
+ surface_at_root_pos(d, event.xmotion.x_root, event.xmotion.y_root, &hpos) : s;
+ drag_set_hover(d, hsurf, hpos);
+ }
+ } else if (event.type == EnterNotify) {
+ mouse.pos = (struct yasl_vector2i){event.xcrossing.x, event.xcrossing.y};
+ mouse.kind = YASL_PTR_MOTION;
+ state = event.xcrossing.state;
+ if (d->drag.active) {
+ struct yasl_vector2i hpos = mouse.pos;
+ struct x11_surface *hsurf = d->native ?
+ surface_at_root_pos(d, event.xcrossing.x_root, event.xcrossing.y_root, &hpos) : s;
+ drag_set_hover(d, hsurf, hpos);
+ }
} else if (event.type == LeaveNotify) {
mouse.pos = (struct yasl_vector2i){event.xcrossing.x, event.xcrossing.y};
mouse.kind = YASL_PTR_LEAVE;
state = event.xcrossing.state;
+ if (d->drag.active)
+ drag_leave_hover(d, s, mouse.pos);
} else {
mouse.pos = (struct yasl_vector2i){event.xbutton.x, event.xbutton.y};
state = event.xbutton.state;
@@ -684,7 +1257,7 @@ static void dispatch(struct yasl_display *base)
mouse.kind = event.type == ButtonPress ? YASL_PTR_PRESS : YASL_PTR_RELEASE;
mouse.button = pointer_button(button);
if (event.type == ButtonRelease && d->drag.active)
- drag_end(d, YASL_DRAG_DROPPED);
+ drag_drop_hover(d);
}
}
d->mods = mouse.mods = modifiers(state);
@@ -696,14 +1269,6 @@ static void dispatch(struct yasl_display *base)
leave(d);
}
-static unsigned long component(uint32_t color, unsigned long mask)
-{
- if (unlikely(!mask)) return 0;
- unsigned shift = (unsigned)__builtin_ctzl(mask);
- unsigned long m = mask >> shift;
- return (((unsigned long)color * m + 127) / 255) << shift;
-}
-
static bool present(struct yasl_surface *base, const struct yasl_framebuffer *fb,
const struct yasl_damage_list *damage)
{
@@ -851,6 +1416,17 @@ static struct yasl_display *display_open(struct yasl_loop *loop,
ATOM(fullscreen, "_NET_WM_STATE_FULLSCREEN");
ATOM(maximized_h, "_NET_WM_STATE_MAXIMIZED_HORZ");
ATOM(maximized_v, "_NET_WM_STATE_MAXIMIZED_VERT");
+ ATOM(xdnd_aware, "XdndAware");
+ ATOM(xdnd_enter, "XdndEnter");
+ ATOM(xdnd_position, "XdndPosition");
+ ATOM(xdnd_status, "XdndStatus");
+ ATOM(xdnd_leave, "XdndLeave");
+ ATOM(xdnd_drop, "XdndDrop");
+ ATOM(xdnd_finished, "XdndFinished");
+ ATOM(xdnd_selection, "XdndSelection");
+ ATOM(xdnd_action_copy, "XdndActionCopy");
+ ATOM(xdnd_type_list, "XdndTypeList");
+ ATOM(uri_list, "text/uri-list");
#undef ATOM
if (d->loop) {
d->watch = yasl_loop_add_fd(d->loop, ConnectionNumber(d->native), POLLIN, ready, &d->base);
@@ -873,6 +1449,7 @@ static void display_close(struct yasl_display *base)
assert(!d->surfaces && "destroy every surface before its display");
drag_cancel(base);
cancel_pastes(d, NULL);
+ d->pending_drop.active = false;
if (d->loop && d->watch > 0) yasl_loop_del_fd(d->loop, d->watch);
if (d->owns_loop && d->loop) {
yasl_loop_destroy(d->loop);
@@ -891,7 +1468,7 @@ static void display_close(struct yasl_display *base)
static uint32_t caps(const struct yasl_display *d)
{
- return YASL_CAP_DRAG | YASL_CAP_DRAG_CARRY | YASL_CAP_PLACE_WINDOW |
+ return YASL_CAP_POPUPS | YASL_CAP_DRAG | YASL_CAP_DRAG_CARRY | YASL_CAP_PLACE_WINDOW |
YASL_CAP_PRIMARY_SELECTION |
(((const struct x11_display *)d)->xi2 ? YASL_CAP_TABLET : 0);
}
@@ -1002,7 +1579,7 @@ static bool place(struct yasl_surface *base, struct yasl_vector2i at)
Window root, child;
int root_x, root_y, x, y;
unsigned mask;
- if (s->destroyed || !XQueryPointer(d->native, s->window,
+ if (s->destroyed || !d->native || !XQueryPointer(d->native, s->window,
&root, &child, &root_x, &root_y, &x, &y, &mask)) return false;
XMoveWindow(d->native, s->window, root_x - at.x, root_y - at.y);
return true;
@@ -1013,9 +1590,31 @@ static void drag_end(struct x11_display *d, enum yasl_drag_end how)
if (!d || !d->drag.active) return;
struct yasl_drag_listener l = d->drag.listener;
void *ud = d->drag.ud;
+ struct x11_surface *hover = d->drag.hover;
+ struct yasl_vector2i hover_pos = d->drag.hover_pos;
+
d->drag.active = false;
+ d->drag.listener = (struct yasl_drag_listener){0};
+ d->drag.hover = NULL;
d->drag.origin = NULL;
d->drag.carried = NULL;
+
+ if (d->drag.icon_window && d->native) {
+ XDestroyWindow(d->native, d->drag.icon_window);
+ d->drag.icon_window = None;
+ }
+ if (d->drag.icon_pixmap && d->native) {
+ XFreePixmap(d->native, d->drag.icon_pixmap);
+ d->drag.icon_pixmap = None;
+ }
+
+ if (hover && !hover->destroyed && hover->cbs && hover->cbs->drag) {
+ if (how == YASL_DRAG_DROPPED)
+ hover->cbs->drag(&hover->base, YASL_DRAG_DROP, hover_pos, hover->ud);
+ else
+ hover->cbs->drag(&hover->base, YASL_DRAG_LEAVE, hover_pos, hover->ud);
+ }
+
if (l.end)
l.end(ud, how);
}
@@ -1024,15 +1623,73 @@ static bool drag_start(struct yasl_display *base, struct yasl_surface *origin,
const struct yasl_drag_image *image,
const struct yasl_drag_listener *l, void *ud)
{
- (void)image;
struct x11_display *d = (struct x11_display *)base;
if (!d || !origin || d->drag.active || !l)
return false;
d->drag.active = true;
d->drag.origin = (struct x11_surface *)origin;
d->drag.carried = NULL;
+ d->drag.hover = NULL;
d->drag.listener = *l;
d->drag.ud = ud;
+ d->drag.icon_window = None;
+ d->drag.icon_pixmap = None;
+
+ if (image && image->pixels && image->size.x > 0 && image->size.y > 0) {
+ d->drag.hotspot = image->hotspot;
+ int rx = 0, ry = 0;
+ if (d->native) {
+ Window root, child;
+ int wx, wy;
+ unsigned mask;
+ XQueryPointer(d->native, RootWindow(d->native, d->screen),
+ &root, &child, &rx, &ry, &wx, &wy, &mask);
+ int x = rx - image->hotspot.x;
+ int y = ry - image->hotspot.y;
+ XSetWindowAttributes attrs = {
+ .override_redirect = True,
+ .save_under = True,
+ };
+ d->drag.icon_window = XCreateWindow(d->native, RootWindow(d->native, d->screen),
+ x, y, (unsigned)image->size.x, (unsigned)image->size.y, 0,
+ CopyFromParent, InputOutput, CopyFromParent,
+ CWOverrideRedirect | CWSaveUnder, &attrs);
+ if (d->drag.icon_window) {
+ GC gc = XCreateGC(d->native, d->drag.icon_window, 0, NULL);
+ XImage *im = XCreateImage(d->native, DefaultVisual(d->native, d->screen),
+ (unsigned)DefaultDepth(d->native, d->screen), ZPixmap, 0, NULL,
+ (unsigned)image->size.x, (unsigned)image->size.y, 32, 0);
+ if (gc && im) {
+ im->data = malloc((size_t)im->bytes_per_line * (size_t)image->size.y);
+ if (im->data) {
+ bool direct = im->bits_per_pixel == 32 && im->red_mask == 0xff0000 &&
+ im->green_mask == 0xff00 && im->blue_mask == 0xff;
+ for (int r = 0; r < image->size.y; r++) {
+ const uint32_t *row = image->pixels + (size_t)r * image->size.x;
+ if (direct) memcpy(im->data + (size_t)r * im->bytes_per_line, row, (size_t)image->size.x * 4);
+ else for (int c = 0; c < image->size.x; c++) {
+ uint32_t p = row[c];
+ XPutPixel(im, c, r, component((p >> 16) & 255, im->red_mask) |
+ component((p >> 8) & 255, im->green_mask) | component(p & 255, im->blue_mask));
+ }
+ }
+ d->drag.icon_pixmap = XCreatePixmap(d->native, RootWindow(d->native, d->screen),
+ (unsigned)image->size.x, (unsigned)image->size.y,
+ (unsigned)DefaultDepth(d->native, d->screen));
+ if (d->drag.icon_pixmap) {
+ XPutImage(d->native, d->drag.icon_pixmap, gc, im, 0, 0, 0, 0,
+ (unsigned)image->size.x, (unsigned)image->size.y);
+ XSetWindowBackgroundPixmap(d->native, d->drag.icon_window, d->drag.icon_pixmap);
+ }
+ }
+ }
+ if (im) XDestroyImage(im);
+ if (gc) XFreeGC(d->native, gc);
+ XMapRaised(d->native, d->drag.icon_window);
+ XFlush(d->native);
+ }
+ }
+ }
return true;
}
@@ -1046,9 +1703,17 @@ static bool drag_carry(struct yasl_surface *base, struct yasl_vector2i offset)
struct x11_display *d = display_of(base);
if (!d || !d->drag.active)
return false;
+ if (d->drag.icon_window && d->native) {
+ XDestroyWindow(d->native, d->drag.icon_window);
+ d->drag.icon_window = None;
+ }
+ if (d->drag.icon_pixmap && d->native) {
+ XFreePixmap(d->native, d->drag.icon_pixmap);
+ d->drag.icon_pixmap = None;
+ }
d->drag.carried = (struct x11_surface *)base;
d->drag.carry_offset = offset;
- return place(base, offset);
+ return d->native ? place(base, offset) : true;
}
static bool interactive_move(struct yasl_surface *base)
@@ -1058,7 +1723,7 @@ static bool interactive_move(struct yasl_surface *base)
Window root, child;
int rx, ry, x, y;
unsigned mask;
- if (s->destroyed || !XQueryPointer(d->native, s->window,
+ if (s->destroyed || !d->native || !XQueryPointer(d->native, s->window,
&root, &child, &rx, &ry, &x, &y, &mask)) return false;
XUngrabPointer(d->native, CurrentTime);
net_message(s, XInternAtom(d->native, "_NET_WM_MOVERESIZE", False), rx, ry, 8, 1, 1);
@@ -1078,6 +1743,8 @@ const struct yasl_backend_ops yasl_x11_backend = {
.display_drag_start = drag_start,
.display_drag_cancel = drag_cancel,
.surface_create_toplevel = create_toplevel,
+ .surface_create_popup = surface_create_popup,
+ .surface_popup_reposition = surface_popup_reposition,
.surface_destroy = surface_destroy,
.surface_display = surface_display,
.surface_set_callbacks = set_callbacks,
diff --git a/tests/x11/test_tablet.c b/tests/x11/test_tablet.c
index 39717df..c35a266 100644
--- a/tests/x11/test_tablet.c
+++ b/tests/x11/test_tablet.c
@@ -9,6 +9,257 @@ static struct yasl_mouse_event next(struct x11_surface *surface)
return event.mouse;
}
+struct drag_test_recorder {
+ int over_count;
+ int leave_count;
+ int drop_count;
+ struct yasl_vector2i last_pos;
+};
+
+static void on_test_drag(struct yasl_surface *s, enum yasl_drag_event ev,
+ struct yasl_vector2i pos, void *ud)
+{
+ (void)s;
+ struct drag_test_recorder *rec = ud;
+ rec->last_pos = pos;
+ if (ev == YASL_DRAG_OVER) rec->over_count++;
+ else if (ev == YASL_DRAG_LEAVE) rec->leave_count++;
+ else if (ev == YASL_DRAG_DROP) rec->drop_count++;
+}
+
+struct drop_test_recorder {
+ int over_count;
+ int leave_count;
+ struct yasl_vector2i last_pos;
+};
+
+static void on_test_drop(struct yasl_surface *s, enum yasl_drop_event ev,
+ struct yasl_vector2i pos, void *ud)
+{
+ (void)s;
+ struct drop_test_recorder *rec = ud;
+ rec->last_pos = pos;
+ if (ev == YASL_DROP_OVER) rec->over_count++;
+ else if (ev == YASL_DROP_LEAVE) rec->leave_count++;
+}
+
+static void on_test_drag_end(void *ud, enum yasl_drag_end how)
+{
+ enum yasl_drag_end *res = ud;
+ *res = how;
+}
+
+static void test_x11_gaps(struct x11_display *d, struct x11_surface *s)
+{
+ /* 1. Caps */
+ assert(caps(&d->base) & YASL_CAP_POPUPS);
+ assert(caps(&d->base) & YASL_CAP_DRAG);
+ assert(caps(&d->base) & YASL_CAP_DRAG_CARRY);
+
+ /* 2. Popups: spec validation, calculation, creation, reposition, destruction */
+ struct yasl_popup_spec invalid_specs[] = {
+ {.size = {0, 100}},
+ {.size = {100, 0}},
+ {.size = {35000, 100}},
+ {.size = {100, 100}, .content_offset = {-1, 0}},
+ {.size = {100, 100}, .content_offset = {100, 0}, .content_size = {10, 0}},
+ {.size = {100, 100}, .anchor = {INT_MIN, 0, 1, 1}},
+ };
+ for (size_t i = 0; i < sizeof(invalid_specs)/sizeof(invalid_specs[0]); i++) {
+ assert(!popup_spec_valid(s, &invalid_specs[i]));
+ assert(!surface_create_popup(&s->base, &invalid_specs[i]));
+ }
+
+ struct yasl_popup_spec valid_spec = {
+ .size = {100, 120},
+ .anchor = {30, 40, 50, 60},
+ .beside = false,
+ .content_offset = {5, 10},
+ };
+ assert(popup_spec_valid(s, &valid_spec));
+ int x = 0, y = 0;
+ popup_calculate_position(d, s, &valid_spec, &x, &y);
+ assert(x == 30 - 5);
+ assert(y == 40 + 60 - 10);
+
+ valid_spec.beside = true;
+ popup_calculate_position(d, s, &valid_spec, &x, &y);
+ assert(x == 30 + 50 - 5);
+ assert(y == 40 - 10);
+
+ struct yasl_surface *popup_base = surface_create_popup(&s->base, &valid_spec);
+ assert(popup_base != NULL);
+ struct x11_surface *popup = (struct x11_surface *)popup_base;
+ assert(popup->is_popup);
+ assert(popup->parent == s);
+ assert(popup->size.x == 100 && popup->size.y == 120);
+
+ struct yasl_popup_spec repo_spec = {
+ .size = {150, 180},
+ .anchor = {10, 20, 30, 40},
+ .beside = false,
+ .content_offset = {0, 0},
+ };
+ assert(surface_popup_reposition(popup_base, &repo_spec));
+ assert(popup->size.x == 150 && popup->size.y == 180);
+
+ surface_destroy(popup_base);
+ assert(d->surfaces == s);
+
+ /* 3. Drag-and-drop target events */
+ struct x11_surface s2 = {.base.display = &d->base, .window = 43};
+ s->next = &s2;
+ s2.next = NULL;
+
+ struct drag_test_recorder rec1 = {0};
+ struct drag_test_recorder rec2 = {0};
+ struct yasl_surface_cbs cbs1 = {.drag = on_test_drag};
+ struct yasl_surface_cbs cbs2 = {.drag = on_test_drag};
+ set_callbacks(&s->base, &cbs1, &rec1);
+ set_callbacks(&s2.base, &cbs2, &rec2);
+
+ enum yasl_drag_end end_reason = (enum yasl_drag_end)-1;
+ struct yasl_drag_listener l = {.end = on_test_drag_end};
+ assert(drag_start(&d->base, &s->base, NULL, &l, &end_reason));
+ assert(d->drag.active);
+
+ /* Motion on s (window 42) -> YASL_DRAG_OVER on s */
+ XIDeviceEvent mev = {.evtype = XI_Motion, .sourceid = 8, .event = 42, .event_x = 12.0, .event_y = 34.0};
+ XGenericEventCookie cookie = {.evtype = XI_Motion, .data = &mev};
+ xi_event(d, &cookie);
+ assert(rec1.over_count == 1);
+ assert(rec1.last_pos.x == 12 && rec1.last_pos.y == 34);
+
+ /* Motion on s2 (window 43) -> YASL_DRAG_LEAVE on s, YASL_DRAG_OVER on s2 */
+ mev.event = 43;
+ mev.event_x = 56.0;
+ mev.event_y = 78.0;
+ xi_event(d, &cookie);
+ assert(rec1.leave_count == 1);
+ assert(rec2.over_count == 1);
+ assert(rec2.last_pos.x == 56 && rec2.last_pos.y == 78);
+
+ /* ButtonRelease -> YASL_DRAG_DROP on s2 and drag end */
+ XIDeviceEvent rel = {.evtype = XI_ButtonRelease, .sourceid = 8, .event = 43, .detail = 1};
+ cookie = (XGenericEventCookie){.evtype = XI_ButtonRelease, .data = &rel};
+ xi_event(d, &cookie);
+ assert(rec2.drop_count == 1);
+ assert(end_reason == YASL_DRAG_DROPPED);
+ assert(!d->drag.active);
+
+ /* Drag cancel */
+ assert(drag_start(&d->base, &s->base, NULL, &l, &end_reason));
+ cookie = (XGenericEventCookie){.evtype = XI_Motion, .data = &mev};
+ mev.event = 42;
+ xi_event(d, &cookie);
+ assert(rec1.over_count == 2);
+ drag_cancel(&d->base);
+ assert(rec1.leave_count == 2);
+ assert(end_reason == YASL_DRAG_CANCELLED);
+ assert(!d->drag.active);
+
+ /* Hierarchy change cancels drag and pushes reset */
+ input_queue_clear(&s->input);
+ assert(drag_start(&d->base, &s->base, NULL, &l, &end_reason));
+ XIHierarchyEvent hiev = {.evtype = XI_HierarchyChanged, .flags = XISlaveRemoved};
+ cookie = (XGenericEventCookie){.evtype = XI_HierarchyChanged, .data = &hiev};
+ xi_event(d, &cookie);
+ assert(end_reason == YASL_DRAG_CANCELLED);
+ assert(!d->drag.active);
+ struct yasl_input_event reset_ev;
+ assert(yasl_surface_input_poll(&s->base, &reset_ev));
+ assert(reset_ev.kind == YASL_INPUT_RESET);
+
+ /* 4. Drag image parameter */
+ uint32_t pixels[16] = {0xFF00FF00};
+ struct yasl_drag_image img = {.pixels = pixels, .size = {4, 4}, .hotspot = {2, 2}};
+ assert(drag_start(&d->base, &s->base, &img, &l, &end_reason));
+ assert(d->drag.active);
+ assert(drag_carry(&s->base, (struct yasl_vector2i){10, 10}));
+ drag_cancel(&d->base);
+
+ /* 5. XDND drop-target events and URI parsing */
+ size_t out_len = 0;
+ char *parsed = parse_file_uris("file:///tmp/doc.txt\r\n", 21, &out_len);
+ assert(parsed && !strcmp(parsed, "file:///tmp/doc.txt\r\n") && out_len == 21);
+ free(parsed);
+
+ parsed = parse_file_uris("# comment\nfile:///a\nfile:///b\n", 30, &out_len);
+ assert(parsed && !strcmp(parsed, "file:///a\r\nfile:///b\r\n") && out_len == 22);
+ free(parsed);
+
+ parsed = parse_file_uris("http://example.com\r\n", 20, &out_len);
+ assert(parsed == NULL && out_len == 0);
+
+ /* Test XDND ClientMessages */
+ d->protocols = 1;
+ d->delete_window = 2;
+ d->xdnd_enter = 10;
+ d->xdnd_position = 11;
+ d->xdnd_leave = 12;
+ d->xdnd_drop = 13;
+ d->uri_list = 20;
+
+ struct drop_test_recorder drec = {0};
+ cbs1.drop = on_test_drop;
+ set_callbacks(&s->base, &cbs1, &drec);
+
+ XClientMessageEvent enter_msg = {
+ .type = ClientMessage,
+ .message_type = d->xdnd_enter,
+ .format = 32,
+ .data.l = {99, 5 << 24, d->uri_list, 0, 0},
+ };
+ handle_client_message(d, s, &enter_msg);
+ assert(d->xdnd.accepts);
+
+ XClientMessageEvent pos_msg = {
+ .type = ClientMessage,
+ .message_type = d->xdnd_position,
+ .format = 32,
+ .data.l = {99, 0, (100 << 16) | 200, 1000, 0},
+ };
+ handle_client_message(d, s, &pos_msg);
+ assert(drec.over_count == 1);
+ assert(drec.last_pos.x == 100 && drec.last_pos.y == 200);
+
+ XClientMessageEvent leave_msg = {
+ .type = ClientMessage,
+ .message_type = d->xdnd_leave,
+ .format = 32,
+ .data.l = {99, 0, 0, 0, 0},
+ };
+ handle_client_message(d, s, &leave_msg);
+ assert(drec.leave_count == 1);
+
+ handle_client_message(d, s, &enter_msg);
+ handle_client_message(d, s, &pos_msg);
+ assert(drec.over_count == 2);
+
+ XClientMessageEvent drop_msg = {
+ .type = ClientMessage,
+ .message_type = d->xdnd_drop,
+ .format = 32,
+ .data.l = {99, 0, 1005, 0, 0},
+ };
+ handle_client_message(d, s, &drop_msg);
+ assert(drec.leave_count == 2);
+ assert(d->pending_drop.active);
+
+ /* Push YASL_INPUT_DROP_FILES */
+ push(s, (struct yasl_input_event){.kind = YASL_INPUT_DROP_FILES}, "file:///sample.png\r\n", 20);
+ struct yasl_input_event drop_ev;
+ assert(yasl_surface_input_poll(&s->base, &drop_ev));
+ assert(drop_ev.kind == YASL_INPUT_DROP_FILES);
+ assert(drop_ev.drop_files.len == 20);
+ assert(!strcmp(drop_ev.drop_files.files, "file:///sample.png\r\n"));
+
+ /* Clean up s2 */
+ s->next = NULL;
+ input_queue_clear(&s->input);
+ input_queue_clear(&s2.input);
+}
+
int main(void)
{
struct x11_display display = {
@@ -99,5 +350,6 @@ int main(void)
assert(tilt_value(&pointer.tilt_x) == 90);
display.pointers = NULL;
input_queue_clear(&surface.input);
+ test_x11_gaps(&display, &surface);
return 0;
}