Table of Contents

OpenTK Input and Window Features


Keys Input

For controls driven by KeyboardManager (e.g., DrawnGame movement), route key events through OpenTkKeyMapper:

protected override void OnKeyDown(KeyboardKeyEventArgs e)
{
    base.OnKeyDown(e);
    if (OpenTkKeyMapper.Map(e.Key) is { } key)
        KeyboardManager.KeyboardPressed(key);
}

protected override void OnKeyUp(KeyboardKeyEventArgs e)
{
    base.OnKeyUp(e);
    if (OpenTkKeyMapper.Map(e.Key) is { } key)
        KeyboardManager.KeyboardReleased(key);
}

DrawnUiWindow handles editor keys (backspace, arrows, home/end, ctrl+A, tab) automatically. Override OnKeyDown and call base.OnKeyDown(e) first, then add your key routing.


Fullscreen

DrawnUiWindow supports fullscreen out of the box:

  • F11 — toggles fullscreen/windowed
  • ESC — exits fullscreen (returns to windowed)
  • System menu (right-click title bar or Alt+Space) — includes a "Fullscreen" item on Windows

To toggle programmatically:

window.ToggleFullscreen();

Window Centering

DrawnUiWindow centers on the primary monitor at startup with no visible flicker. The window starts hidden, positions itself, then becomes visible.


DWM Title Bar Styling (Windows)

Override ConfigureWindowChrome in your DrawnUiWindow subclass to apply custom DWM colors:

class MyWindow(GameWindowSettings gs, NativeWindowSettings ns, Canvas canvas)
    : DrawnUiWindow(gs, ns, canvas)
{
    [SupportedOSPlatform("windows")]
    protected override void ConfigureWindowChrome(IntPtr hwnd)
    {
        // Match your app's background color (0x1A, 0x1A, 0x2E = #1A1A2E)
        WindowChrome.SetCaptionColor(hwnd, 0x1A, 0x1A, 0x2E);
        WindowChrome.SetBorderColor(hwnd, 0x1A, 0x1A, 0x2E);
    }
}

WindowChrome helpers:

Method Effect Min Windows
SetCaptionColor(hwnd, r, g, b) Title bar background color Win11 22000
SetBorderColor(hwnd, r, g, b) Window border color Win11 22000
SetDarkMode(hwnd, bool) Force dark/light title text Win10 20H1
SetRoundedCorners(hwnd, bool) Rounded/square corners Win11 22000

When a custom caption color is set, Windows automatically picks black or white title text based on luminance. You do not need SetDarkMode.

ConfigureWindowChrome is only called on Windows — no OperatingSystem.IsWindows() guard is needed inside the override.


Publish — Self-Contained Single File

For a distributable release build targeting Windows x64:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
  <SelfContained>true</SelfContained>
  <PublishTrimmed>true</PublishTrimmed>
  <TrimMode>full</TrimMode>
  <PublishSingleFile>true</PublishSingleFile>
  <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>

TrimMode=full will cut SkiaSharp font and image APIs unless you protect them:

<ItemGroup Condition="'$(PublishTrimmed)' == 'true'">
  <TrimmerRootAssembly Include="OpenTK.Graphics" />
  <TrimmerRootAssembly Include="OpenTK.Windowing.Desktop" />
  <TrimmerRootAssembly Include="SkiaSharp" />
  <TrimmerRootAssembly Include="HarfBuzzSharp" />
</ItemGroup>

To strip PDB files from the publish output:

<Target Name="ExcludePdbsFromPublish" AfterTargets="ComputeFilesToPublish">
  <ItemGroup>
    <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
                           Condition="'%(Extension)' == '.pdb'" />
  </ItemGroup>
</Target>

Suppress the console window on Windows:

<OutputType>WinExe</OutputType>

Window Icon

Set the file system icon (Explorer, taskbar) via ApplicationIcon and embed it as a resource for the title bar:

<PropertyGroup>
  <ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>

<ItemGroup>
  <EmbeddedResource Include="icon.ico" />
</ItemGroup>

Load the embedded ICO at runtime and pass it to NativeWindowSettings.Icon:

OpenTK.Windowing.Common.Input.WindowIcon? LoadWindowIcon()
{
    try
    {
        var asm = System.Reflection.Assembly.GetExecutingAssembly();
        var name = asm.GetManifestResourceNames()
            .FirstOrDefault(n => n.EndsWith("icon.ico", StringComparison.OrdinalIgnoreCase));
        if (name == null) return null;

        using var stream = asm.GetManifestResourceStream(name);
        if (stream == null) return null;

        using var bitmap = SKBitmap.Decode(stream);
        if (bitmap == null) return null;

        var resized = bitmap.Width != 32 || bitmap.Height != 32
            ? bitmap.Resize(new SKImageInfo(32, 32), SKFilterQuality.High)
            : bitmap;

        var pixels = resized.Bytes;
        // SKBitmap is BGRA; OpenTK wants RGBA
        for (int i = 0; i < pixels.Length; i += 4)
            (pixels[i], pixels[i + 2]) = (pixels[i + 2], pixels[i]);

        var image = new OpenTK.Windowing.Common.Input.Image(32, 32, pixels);
        if (!ReferenceEquals(resized, bitmap)) resized.Dispose();
        return new OpenTK.Windowing.Common.Input.WindowIcon(image);
    }
    catch { return null; }
}

Then:

var nativeSettings = new NativeWindowSettings
{
    Icon = LoadWindowIcon(),
    // ...
};

Game Timing

Frame time starts from OnLoad(), not system boot. DrawnGame.LastFrameTimeNanos begins near 0 and the first delta is a few milliseconds — matching MAUI and Blazor behavior.