From b20263cf9b6d85c7e5919497b5d36b4c81463b15 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 13:00:53 +0300 Subject: 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 --- src/cocoa/window.m | 519 +++++++++++++++++++++++++++++++++++--- src/x11/window.c | 727 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 1180 insertions(+), 66 deletions(-) (limited to 'src') 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 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 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)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)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)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 *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, -- cgit v1.2.3