Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/Skyline.Apple/AppKitWindowBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,57 @@ public string? ClipboardText
}
}

// MIME type to macOS pasteboard UTI, for the formats a browser moves most.
private static readonly (string Mime, string Uti)[] ClipboardTypeMap =
[
("text/plain", PasteboardText),
("text/html", "public.html"),
("image/png", "public.png"),
];

private static string? MimeToUti(string mime)
{
foreach (var (m, uti) in ClipboardTypeMap)
{
if (m == mime)
{
return uti;
}
}
return null;
}

public IReadOnlyList<string> ClipboardFormats
{
get
{
var pb = NSPasteboard.GeneralPasteboard;
var formats = new List<string>();
foreach (var (mime, uti) in ClipboardTypeMap)
{
if (pb.GetDataForType(uti) is not null)
{
formats.Add(mime);
}
}
return formats;
}
}

public byte[]? GetClipboardData(string mimeType) =>
MimeToUti(mimeType) is { } uti ? NSPasteboard.GeneralPasteboard.GetDataForType(uti)?.ToArray() : null;

public void SetClipboardData(string mimeType, byte[] data)
{
if (MimeToUti(mimeType) is not { } uti)
{
return;
}
var pb = NSPasteboard.GeneralPasteboard;
pb.ClearContents();
pb.SetDataForType(NSData.FromArray(data), uti);
}

public bool IsClosing => _closing;

public WindowSurfaceSource SurfaceSource => _surfaceSource;
Expand Down
15 changes: 15 additions & 0 deletions src/Skyline/AppWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,21 @@ public string? ClipboardText
set => _backend.ClipboardText = value;
}

/// <summary>The MIME types currently on the clipboard.</summary>
public IReadOnlyList<string> ClipboardFormats => _backend.ClipboardFormats;

/// <summary>
/// Read clipboard data for a MIME type (for example "image/png" or
/// "text/html"), or null if the clipboard holds no such type.
/// </summary>
public byte[]? GetClipboardData(string mimeType) => _backend.GetClipboardData(mimeType);

/// <summary>
/// Write clipboard data under a MIME type, replacing the clipboard. The
/// GLFW backend stores text only; other types are dropped there.
/// </summary>
public void SetClipboardData(string mimeType, byte[] data) => _backend.SetClipboardData(mimeType, data);

public FrameInfo CurrentFrame => Frame(0);

/// <summary>True when this window has been adopted by an <see cref="AppHost"/>. Hosted windows render on the host's thread, so use <c>AppHost.Run</c>, not <see cref="Run"/>.</summary>
Expand Down
25 changes: 25 additions & 0 deletions src/Skyline/GlfwWindowBackend.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
using System.Text;
using Silk.NET.GLFW;
using Silk.NET.Input;
using Silk.NET.Maths;
Expand Down Expand Up @@ -88,6 +89,30 @@ public unsafe string? ClipboardText
set => _glfw.SetClipboardString(_glfwHandle, value ?? string.Empty);
}

// GLFW's clipboard is text only, so it reports and stores text/plain alone.
private static readonly string[] TextFormats = ["text/plain"];

public unsafe IReadOnlyList<string> ClipboardFormats =>
string.IsNullOrEmpty(_glfw.GetClipboardString(_glfwHandle)) ? [] : TextFormats;

public unsafe byte[]? GetClipboardData(string mimeType)
{
if (mimeType == "text/plain" && _glfw.GetClipboardString(_glfwHandle) is { Length: > 0 } s)
{
return Encoding.UTF8.GetBytes(s);
}
return null;
}

public unsafe void SetClipboardData(string mimeType, byte[] data)
{
// Other MIME types are dropped: GLFW has nowhere to put them.
if (mimeType == "text/plain")
{
_glfw.SetClipboardString(_glfwHandle, Encoding.UTF8.GetString(data));
}
}

public bool IsClosing => _window.IsClosing;

public WindowSurfaceSource SurfaceSource => _surfaceSource;
Expand Down
9 changes: 9 additions & 0 deletions src/Skyline/IWindowBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ internal interface IWindowBackend : IDisposable
string Title { get; set; }
string? ClipboardText { get; set; }

/// <summary>The MIME types currently on the clipboard.</summary>
IReadOnlyList<string> ClipboardFormats { get; }

/// <summary>Read clipboard data for a MIME type, or null if absent.</summary>
byte[]? GetClipboardData(string mimeType);

/// <summary>Write clipboard data under a MIME type, replacing the clipboard.</summary>
void SetClipboardData(string mimeType, byte[] data);

bool IsClosing { get; }

/// <summary>How a presenter builds a surface for this window.</summary>
Expand Down
3 changes: 3 additions & 0 deletions tests/Skyline.Tests/WindowBackendFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ private sealed class FakeBackend : IWindowBackend
public (int Width, int Height) LogicalSize => (100, 50);
public string Title { get; set; } = "fake";
public string? ClipboardText { get; set; }
public IReadOnlyList<string> ClipboardFormats => [];
public byte[]? GetClipboardData(string mimeType) => null;
public void SetClipboardData(string mimeType, byte[] data) { }
public bool IsClosing => false;
public WindowSurfaceSource SurfaceSource { get; } = new WindowSurfaceSource.MetalLayer(0x1234);

Expand Down
11 changes: 11 additions & 0 deletions tests/Skyline.WindowedTests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ void Check(bool condition, string what)
win.ClipboardText = "skyline-test";
Check(win.ClipboardText == "skyline-test", "clipboard round-trips");

// Typed clipboard on the GLFW backend (text only there).
using (var clipWin = new AppWindow(new AppWindowOptions { Title = "clip", Width = 160, Height = 120, ForceGlfw = true }))
{
clipWin.SetClipboardData("text/plain", System.Text.Encoding.UTF8.GetBytes("typed-clip"));
var back = clipWin.GetClipboardData("text/plain");
Check(back is not null && System.Text.Encoding.UTF8.GetString(back) == "typed-clip", "typed text clipboard round-trips on GLFW");
Check(clipWin.GetClipboardData("image/png") is null, "GLFW reports no data for an unsupported type");
clipWin.SetClipboardData("image/png", [1, 2, 3]); // dropped, must not throw
Check(clipWin.ClipboardFormats.Contains("text/plain"), "ClipboardFormats lists text/plain when text is present");
}

win.PumpEvents(); // must not throw without a host

// --- Chrome modes construct on the GLFW backend ------------------------
Expand Down