Writing an engine integration
IcyUI draws and reads input through a few interfaces, and an engine integration implements them. This guide lists what
one provides, in the order you build it, using FNA as the target. FNA has the XNA API, so the skeleton looks like the
MonoGame integration, which is the complete reference:
IcyUI.MonoGame on GitHub.
Note
IcyUI.FNA is a stub today; there is no FNA package yet. This guide describes how an integration is built, not a
shipped one. FNA-specific details, such as its native libraries, are out of scope.
You need a good grasp of the engine you target and of Canvas, the host and the element tree. A game never needs any of this: it only uses an integration that exists.
What an integration provides
| Piece | Interface | Does |
|---|---|---|
| Drawing | IRenderContext |
Draws textured quads inside a sprite batch, makes textures, reports the viewport and display scale. |
| Input | IInputSystem |
Turns keyboard, mouse, gamepad and touch state into IcyUI's events; text input and the clipboard. |
| Assets | IAssetContextFactory, IPlatformAssetImporter<T> |
Opens the engine's content and turns image streams into textures. |
| Host | IIcyHost |
A game component that connects the three to the game loop and owns the canvases. |
| Entry point | UseIcyUI(this Game, ...) |
Builds the configuration and adds the host. |
IcyUI's core never references the engine; everything above is the only code that does.
1. The render context
IRenderContext draws only textured quads: the canvas calls Begin, then Draw for each rectangle, glyph and image,
then End. A solid color is a tinted draw of WhiteTexture.
/// <summary>What an engine gives IcyUI to draw with. Everything IcyUI draws is textured quads in this order: Begin, many Draw, End.</summary>
public abstract class EngineRenderContext : IRenderContext
{
public abstract event EventHandler? ViewportResize;
public abstract IRenderOptions Options { get; } // opacity, scissor rectangle: the engine applies them to what follows
public abstract ITexture WhiteTexture { get; } // solid fills are tinted draws of this
public abstract Transform2D Transform { get; set; } // applied to every Draw
public abstract Size ViewportSize { get; } // the back buffer, in pixels
public abstract void Begin(); // open a sprite batch (straight alpha, scissor test on)
public abstract void Draw(ITexture texture, in TextureRenderingOptions options); // one quad
public abstract void End(); // submit the batch
public abstract void Flush(); // End then Begin, when a state changes mid-frame (an effect, the scissor)
public abstract ITexture CreateTexture<TColor>(int width, int height, TColor[] data)
where TColor : unmanaged; // font atlases and generated images
public abstract void ApplyEffect(IEffect effect);
public abstract void ClearEffects();
public abstract IEffect GetBuiltInEffect(EffectCode code);
public abstract void Dispose();
}
Points that went wrong before, so look at them in the MonoGame version:
- Straight alpha. IcyUI's textures aren't premultiplied, so the sprite batch must blend with straight alpha; the default premultiplied blend makes anti-aliased text edges too bright.
- Scissor test. Clipping uses the scissor rectangle, and the rasterizer state must enable the scissor test or every clip is silently ignored.
Flushis a no-op outsideBegin/End; it ends and restarts the batch when the scissor or an effect changes.- Display scale. Report the monitor's DPI scale (
DisplayScale,DisplayScaleChanged), polled each update, so scaling works. Core supplies the platform providers.
2. Input
/// <summary>What an engine gives IcyUI to read input from. Each device turns the engine's state into IcyUI's events.</summary>
public abstract class EngineInputSystem : IInputSystem, IUpdateableInput
{
public abstract IInputEventSystem Events { get; } // the events canvases subscribe to: pointer, keys, navigation, text
public abstract IKeyboardInput? Keyboard { get; }
public abstract IMouseInput Mouse { get; }
public abstract IGamepadInput? Gamepad { get; } // null when the engine has no gamepads
public abstract ITouchInput? Touch { get; } // null when the engine reports no touch
public abstract IClipboard Clipboard { get; }
public abstract bool IsInitialized { get; }
public abstract void Initialize(); // hook the engine: window text input, device state
public abstract void Update(TimeSpan deltaTime); // poll the devices; called once per game update
public abstract bool IsDeviceAvailable<T>()
where T : IInputDeviceListener;
public abstract T GetInputDevice<T>()
where T : IInputDeviceListener;
public abstract bool TryGetInputDevice<T>([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out T? device)
where T : IInputDeviceListener;
public abstract IEnumerator<IInputDeviceListener> GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
Each device implements its interface by reading the engine's state in Update and raising IcyUI's events (pressed,
released, moved, navigation). Text input comes from the engine's window (character events, IME) rather than from key
presses. Leave a device null if the engine can't report it; the canvas copes. The clipboard is the one piece with an
OS dependency: pick it with the core's clipboard selector instead of writing one per engine.
3. Assets
An asset context opens content by URI (the engine's content pipeline, files, embedded resources), and an importer turns
an image stream into the engine's texture. Register both on the asset configuration; IcyUI.MonoGame has a
MonoGameAssetContextFactory and a TextureImporter of a few lines each.
4. The host
The host connects the pieces to the game loop. It draws last, so the UI is on top of the game:
/// <summary>The game component that runs IcyUI: input and display scale in Update, every canvas in Draw, after the game's own drawing.</summary>
public sealed class EngineIcyHost : DrawableGameComponent, IIcyHost
{
public EngineIcyHost(Game game, IcyConfiguration configuration)
: base(game)
{
Configuration = configuration;
MainCanvas = new Canvas(configuration) { IsInputEnabled = true };
Canvases = [MainCanvas];
DrawOrder = int.MaxValue; // drawn last, so the UI is on top of the game
}
public IcyConfiguration Configuration { get; }
public Canvas MainCanvas { get; }
public IList<Canvas> Canvases { get; }
public override void Initialize()
{
Configuration.Input.Initialize();
base.Initialize();
}
public override void Update(GameTime gameTime)
{
if (Configuration.Input is IUpdateableInput updateable)
updateable.Update(gameTime.ElapsedGameTime);
base.Update(gameTime);
}
public override void Draw(GameTime gameTime)
{
for (int i = 0; i < Canvases.Count; i++) // by index: UI code may add a canvas during the frame
Canvases[i].Render();
base.Draw(gameTime);
}
}
5. The entry point
The extension builds the configuration with the engine's defaults, then runs the game's lambda so it can replace any of them, then adds the host:
/// <summary>The entry point a game calls: build the configuration, let the game adjust it, add the host.</summary>
public static class EngineBuildingExtensions
{
public static EngineIcyHost UseIcyUI(this Game game, Action<IConfigurationBuilder> configure)
{
var builder = new IcyConfigurationBuilder();
builder.ConfigureRendering(CreateRenderContext(game)) // your IRenderContext
.ConfigureInput(CreateInputSystem(game)) // your IInputSystem
.ConfigureAssets();
builder.AddMarkupSupport();
builder.UseDefaultTheme();
configure(builder); // the game's lambda runs after the defaults, so it can replace them
var host = new EngineIcyHost(game, builder.Build());
game.Components.Add(host);
return host;
}
private static IRenderContext CreateRenderContext(Game game) => throw new NotImplementedException();
private static IInputSystem CreateInputSystem(Game game) => throw new NotImplementedException();
}
Throw if the game calls it twice, and offer GetIcyHost and GetIcyCanvas so code can reach the host later; the
MonoGame version shows both.
What you built
The five pieces an engine needs, in the order that lets you test each: draw a rectangle, read a click, load a texture, then run a markup page through the host.
Next
- Canvas, the host and the element tree: what the host does with the canvases.
- Configuration: the builder the entry point fills.