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
23 changes: 23 additions & 0 deletions src/Skyline.Apple/AppKitWindowBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal sealed class AppKitWindowBackend : IWindowBackend

public event Action<(int Width, int Height)>? FramebufferResized;
public event Action<bool>? MinimizedChanged;
public event Action<(int X, int Y)>? Moved;
public event Action<double>? Render;
public event Action<PointerEvent>? Pointer;
public event Action<KeyEvent>? Key;
Expand Down Expand Up @@ -154,6 +155,24 @@ public void Run()

public void Resize(int width, int height) => _window.SetContentSize(new CGSize(width, height));

// Skyline reports a top-left origin (like GLFW); AppKit's frame is
// bottom-left, so flip against the window's screen height.
public (int X, int Y) Position
{
get
{
var f = _window.Frame;
var screenHeight = (_window.Screen ?? NSScreen.MainScreen)?.Frame.Height ?? f.Height;
return ((int)f.X, (int)(screenHeight - f.Y - f.Height));
}
set
{
var frame = _window.Frame;
var screenHeight = (_window.Screen ?? NSScreen.MainScreen)?.Frame.Height ?? frame.Height;
_window.SetFrameOrigin(new CGPoint(value.X, screenHeight - value.Y - frame.Height));
}
}

public void Minimize() => _window.Miniaturize(null);

public void Restore() => _window.Deminiaturize(null);
Expand All @@ -162,6 +181,8 @@ public void Run()

internal void OnMinimizedChanged(bool minimized) => MinimizedChanged?.Invoke(minimized);

internal void OnMoved() => Moved?.Invoke(Position);

public void Dispose()
{
_window.Delegate = null;
Expand All @@ -181,5 +202,7 @@ private sealed class WindowDelegate(AppKitWindowBackend backend) : NSWindowDeleg
public override void DidMiniaturize(NSNotification notification) => backend.OnMinimizedChanged(true);

public override void DidDeminiaturize(NSNotification notification) => backend.OnMinimizedChanged(false);

public override void DidMove(NSNotification notification) => backend.OnMoved();
}
}
26 changes: 26 additions & 0 deletions src/Skyline/AppWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public AppWindow(AppWindowOptions? options = null)
_backend.Pointer += e => PointerInput?.Invoke(e);
_backend.Key += e => KeyInput?.Invoke(e);
_backend.Text += e => TextInput?.Invoke(e);
_backend.Moved += p => Moved?.Invoke(p);
}

internal void RaisePointer(PointerEventKind kind, float x, float y, int button, float wheelDx, float wheelDy) =>
Expand Down Expand Up @@ -139,6 +140,9 @@ internal void RaiseText(char character) =>
public event Action<KeyEvent>? KeyInput;
public event Action<TextEvent>? TextInput;

/// <summary>The window moved. Carries the new top-left in screen points.</summary>
public event Action<(int X, int Y)>? Moved;

/// <summary>
/// When set and returning false, the frame is skipped and the loop
/// sleeps briefly instead of rendering. Continuous-render apps leave
Expand Down Expand Up @@ -186,6 +190,28 @@ public int Run()
/// <summary>Resize to a logical size. Main thread only; <see cref="Resized"/> follows.</summary>
public void Resize(int width, int height) => _backend.Resize(width, height);

/// <summary>
/// The window's top-left on screen, in logical points. Set to move the
/// window; <see cref="Moved"/> follows. Save it on close and restore it on
/// the next launch to keep the window where the user left it.
/// </summary>
public (int X, int Y) Position
{
get => _backend.Position;
set => _backend.Position = value;
}

/// <summary>The window's screen rect in logical points: <see cref="Position"/> plus its logical size.</summary>
public (int X, int Y, int Width, int Height) Bounds
{
get
{
var (x, y) = _backend.Position;
var (w, h) = _backend.LogicalSize;
return (x, y, w, h);
}
}

/// <summary>Minimize the window. Main thread only.</summary>
public void Minimize()
{
Expand Down
8 changes: 8 additions & 0 deletions src/Skyline/GlfwWindowBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal sealed class GlfwWindowBackend : IWindowBackend

public event Action<(int Width, int Height)>? FramebufferResized;
public event Action<bool>? MinimizedChanged;
public event Action<(int X, int Y)>? Moved;
public event Action<double>? Render;
public event Action<PointerEvent>? Pointer;
public event Action<KeyEvent>? Key;
Expand Down Expand Up @@ -51,6 +52,7 @@ public unsafe GlfwWindowBackend(AppWindowOptions options)

_window.FramebufferResize += sz => FramebufferResized?.Invoke((sz.X, sz.Y));
_window.StateChanged += state => MinimizedChanged?.Invoke(state == WindowState.Minimized);
_window.Move += pos => Moved?.Invoke((pos.X, pos.Y));
_window.Render += delta => Render?.Invoke(delta);

_input = _window.CreateInput();
Expand All @@ -72,6 +74,12 @@ public unsafe GlfwWindowBackend(AppWindowOptions options)
public (int Width, int Height) FramebufferSize => (_window.FramebufferSize.X, _window.FramebufferSize.Y);
public (int Width, int Height) LogicalSize => (_window.Size.X, _window.Size.Y);

public (int X, int Y) Position
{
get => (_window.Position.X, _window.Position.Y);
set => _window.Position = new Vector2D<int>(value.X, value.Y);
}

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

/// <summary>Top-left of the window in screen coordinates, in logical points.</summary>
(int X, int Y) Position { get; set; }

bool IsClosing { get; }

/// <summary>How a presenter builds a surface for this window.</summary>
Expand All @@ -31,6 +34,9 @@ internal interface IWindowBackend : IDisposable
/// <summary>Minimized state changed; true when minimized.</summary>
event Action<bool>? MinimizedChanged;

/// <summary>The window moved; carries the new top-left in screen points.</summary>
event Action<(int X, int Y)>? Moved;

/// <summary>A frame tick from the backend's own loop, with the delta seconds.</summary>
event Action<double>? Render;

Expand Down
2 changes: 2 additions & 0 deletions tests/Skyline.Tests/WindowBackendFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ 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 (int X, int Y) Position { get; set; }
public bool IsClosing => false;
public WindowSurfaceSource SurfaceSource { get; } = new WindowSurfaceSource.MetalLayer(0x1234);

public event Action<(int Width, int Height)>? FramebufferResized;
public event Action<bool>? MinimizedChanged;
public event Action<(int X, int Y)>? Moved;
public event Action<double>? Render;
public event Action<PointerEvent>? Pointer;
public event Action<KeyEvent>? Key;
Expand Down
12 changes: 12 additions & 0 deletions tests/Skyline.WindowedTests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ void Check(bool condition, string what)
Check(soloResized, "Resize fires Resized in single-window mode");
frame = win.CurrentFrame;

// --- Window position and bounds on the GLFW backend --------------------

using (var posWin = new AppWindow(new AppWindowOptions { Title = "pos", Width = 160, Height = 120, ForceGlfw = true }))
{
posWin.Position = (140, 110);
posWin.PumpEvents();
var p = posWin.Position;
Check(p.X >= 0 && p.Y >= 0, "Position get/set round-trips on the GLFW backend");
var bounds = posWin.Bounds;
Check(bounds is { Width: > 0, Height: > 0 } && (bounds.X, bounds.Y) == p, "Bounds reports position plus logical size");
}

// --- Input raising (the seam below GLFW callbacks) ---------------------

PointerEvent? pointer = null;
Expand Down