See Unicode, layout, and update hardening for the current Rune input, numeric-range, catch-up, and text-metrics contracts.
This guide explains how to create a custom graphics backend for FishUI. FishUI is designed to be renderer-agnostic, allowing you to use it with any graphics library (Raylib, MonoGame, SDL, OpenGL, etc.).
- Overview
- Quick Start with SimpleFishUIGfx
- Minimum Required Methods
- Optional Overrides for Performance
- Complete Example: Raylib Backend
- Implementing IFishUIGfx Directly
- Input Backend
- Best Practices
FishUI separates rendering from the UI logic through the IFishUIGfx interface. To use FishUI with your graphics library, you need to implement this interface.
There are two approaches:
-
SimpleFishUIGfx (Recommended) - Extend this abstract class that provides default implementations for many methods. You only need to implement ~10 core methods.
-
IFishUIGfx (Direct) - Implement the full interface with 25+ methods for maximum control and performance.
The easiest way to create a backend is to extend SimpleFishUIGfx:
using FishUI;
using System.Numerics;
public class MyGfx : SimpleFishUIGfx
{
public override void Init()
{
// Initialize your graphics system
}
public override int GetWindowWidth() => /* your window width */;
public override int GetWindowHeight() => /* your window height */;
public override ImageRef LoadImage(string FileName)
{
// Load image and return ImageRef with Width, Height, and Userdata
return new ImageRef(FileName, imageWidth, imageHeight, nativeTexture, cpuImage);
}
public override FontRef LoadFont(string FileName, float Size, float Spacing, FishColor Color)
{
// Load font and return FontRef
return new FontRef(FileName, nativeFont, Size, Spacing, Color,
FontStyle.Regular, isMonospaced: false, lineHeight: Size);
}
public override void BeginScissor(Vector2 Pos, Vector2 Size)
{
// Set scissor/clipping rectangle
}
public override void EndScissor()
{
// Clear scissor/clipping
}
public override void DrawLine(Vector2 Pos1, Vector2 Pos2, float Thick, FishColor Clr)
{
// Draw a line
}
public override void DrawRectangle(Vector2 Position, Vector2 Size, FishColor Color)
{
// Draw a filled rectangle
}
public override void DrawImage(ImageRef Img, Vector2 Pos, float Rot, float Scale, FishColor Color)
{
// Draw an image (Img.Userdata contains your native texture)
}
public override Vector2 MeasureText(FontRef Fn, string Text)
{
// Measure text size and return dimensions
return new Vector2(/* width */, /* height */);
}
public override void DrawTextColorScale(FontRef Fn, string Text, Vector2 Pos, FishColor Color, float Scale)
{
// Draw text with color and scale (Fn.Userdata contains your native font)
}
}When extending SimpleFishUIGfx, you must implement these methods:
| Method | Purpose |
|---|---|
Init() |
Initialize your graphics system |
GetWindowWidth() |
Return current window width |
GetWindowHeight() |
Return current window height |
LoadImage(string) |
Load an image file into an ImageRef |
LoadFont(...) |
Load a font file into a FontRef |
BeginScissor(...) |
Set scissor/clipping rectangle |
EndScissor() |
Clear scissor/clipping |
DrawLine(...) |
Draw a line between two points |
DrawRectangle(...) |
Draw a filled rectangle |
DrawImage(...) |
Draw an image with rotation and scale |
MeasureText(...) |
Measure text dimensions |
DrawTextColorScale(...) |
Draw text with color and scale |
When loading images, populate these fields:
public override ImageRef LoadImage(string FileName)
{
var texture = YourGraphicsLib.LoadTexture(FileName);
return new ImageRef(FileName, texture.Width, texture.Height, texture);
}When loading fonts, populate these fields:
public override FontRef LoadFont(string FileName, float Size, float Spacing, FishColor Color)
{
var font = YourGraphicsLib.LoadFont(FileName, (int)Size);
return new FontRef(FileName, font, Size, Spacing, Color,
FontStyle.Regular, isMonospaced: false, lineHeight: Size);
}SimpleFishUIGfx provides default implementations for these methods, but you can override them for better performance:
| Method | Default Implementation | Override For |
|---|---|---|
BeginDrawing(float) |
Does nothing | Frame begin (clear screen, etc.) |
EndDrawing() |
Does nothing | Frame end (swap buffers, etc.) |
DrawRectangleOutline(...) |
4 DrawLine calls |
Native outline drawing |
DrawCircle(...) |
Polygon approximation | Native filled circle |
DrawCircleOutline(...) |
Polygon with lines | Native circle outline |
DrawNPatch(...) |
9 DrawImageRegion calls |
Native 9-slice support |
DrawImage(with Size) |
Calculates scale | Proper source rect drawing |
DrawText(...) |
Calls DrawTextColor |
Direct text drawing |
DrawTextColor(...) |
Calls DrawTextColorScale |
Direct colored text |
PushScissor(...) |
Stack with intersection | Native scissor stack |
PopScissor() |
Stack-based | Native scissor stack |
GetFontMetrics(...) |
Estimates from MeasureText |
Accurate font metrics |
SetImageFilter(...) |
Does nothing | Texture filtering |
GetImageColor(...) |
Throws | Pixel color reading |
public override void DrawNPatch(NPatch NP, Vector2 Pos, Vector2 Size, FishColor Color, float Rotation)
{
Texture2D tex = (Texture2D)NP.Image.Userdata;
NPatchInfo info = new NPatchInfo
{
Left = NP.Left,
Right = NP.Right,
Top = NP.Top,
Bottom = NP.Bottom,
Source = new Rectangle(NP.ImagePos, NP.ImageSize),
Layout = NPatchLayout.NinePatch
};
Raylib.DrawTextureNPatch(tex, info, new Rectangle(Pos, Size), Vector2.Zero, Rotation,
new Color(Color.R, Color.G, Color.B, Color.A));
}Here's a complete minimal Raylib backend using SimpleFishUIGfx:
using FishUI;
using Raylib_cs;
using System;
using System.Numerics;
public class RaylibGfxMinimal : SimpleFishUIGfx
{
private int _width, _height;
private string _title;
public RaylibGfxMinimal(int width, int height, string title)
{
_width = width;
_height = height;
_title = title;
}
#region Required Overrides
public override void Init()
{
Raylib.InitWindow(_width, _height, _title);
Raylib.SetTargetFPS(60);
}
public override int GetWindowWidth() => Raylib.GetScreenWidth();
public override int GetWindowHeight() => Raylib.GetScreenHeight();
public override ImageRef LoadImage(string FileName)
{
Texture2D tex = Raylib.LoadTexture(FileName);
return new ImageRef
{
Path = FileName,
Width = tex.Width,
Height = tex.Height,
Userdata = tex
};
}
public override FontRef LoadFont(string FileName, float Size, float Spacing, FishColor Color)
{
Font font = Raylib.LoadFontEx(FileName, (int)Size, null, 250);
return new FontRef
{
Path = FileName,
Size = Size,
Spacing = Spacing,
Color = Color,
Userdata = font,
LineHeight = font.BaseSize
};
}
public override void BeginScissor(Vector2 Pos, Vector2 Size)
{
Raylib.BeginScissorMode((int)Pos.X, (int)Pos.Y, (int)Size.X, (int)Size.Y);
}
public override void EndScissor()
{
Raylib.EndScissorMode();
}
public override void DrawLine(Vector2 Pos1, Vector2 Pos2, float Thick, FishColor Clr)
{
Raylib.DrawLineEx(Pos1, Pos2, Thick, ToColor(Clr));
}
public override void DrawRectangle(Vector2 Position, Vector2 Size, FishColor Color)
{
Raylib.DrawRectangleV(Position, Size, ToColor(Color));
}
public override void DrawImage(ImageRef Img, Vector2 Pos, float Rot, float Scale, FishColor Color)
{
Texture2D tex = (Texture2D)Img.Userdata;
Raylib.DrawTextureEx(tex, Pos, Rot, Scale, ToColor(Color));
}
public override Vector2 MeasureText(FontRef Fn, string Text)
{
Font font = (Font)Fn.Userdata;
return Raylib.MeasureTextEx(font, Text, Fn.Size, Fn.Spacing);
}
public override void DrawTextColorScale(FontRef Fn, string Text, Vector2 Pos, FishColor Color, float Scale)
{
Font font = (Font)Fn.Userdata;
Raylib.DrawTextEx(font, Text, Pos, Fn.Size * Scale, Fn.Spacing * Scale, ToColor(Color));
}
#endregion
#region Optional Performance Overrides
public override void BeginDrawing(float Dt)
{
Raylib.BeginDrawing();
Raylib.ClearBackground(Color.White);
}
public override void EndDrawing()
{
Raylib.EndDrawing();
}
public override void DrawRectangleOutline(Vector2 Position, Vector2 Size, FishColor Color)
{
Raylib.DrawRectangleLinesEx(new Rectangle(Position, Size), 1, ToColor(Color));
}
public override void DrawCircle(Vector2 Center, float Radius, FishColor Color)
{
Raylib.DrawCircleV(Center, Radius, ToColor(Color));
}
#endregion
private static Color ToColor(FishColor c) => new Color(c.R, c.G, c.B, c.A);
}For maximum control, implement IFishUIGfx directly. See FishUI/IFishUIGfx.cs for the complete interface definition.
This approach requires implementing all 25+ methods but gives you full control over every aspect of rendering.
Implement IFishUIInput for pointer, keyboard, text, and touch input.
using System;
using FishUI;
using System.Numerics;
public class MyInput : IFishUIInput
{
public FishKey GetKeyPressed() => ReadNextPressedKey();
public int GetCharPressed() => ReadNextGeneratedCharacter();
public bool IsKeyDown(FishKey key) => ReadKeyDown(key);
public bool IsKeyUp(FishKey key) => !ReadKeyDown(key);
public bool IsKeyPressed(FishKey key) => ReadKeyPressed(key);
public bool IsKeyReleased(FishKey key) => ReadKeyReleased(key);
public Vector2 GetMousePosition() => ReadMousePosition();
public float GetMouseWheelMove() => ReadWheelDelta();
public bool IsMouseDown(FishMouseButton button) => ReadMouseDown(button);
public bool IsMouseUp(FishMouseButton button) => !ReadMouseDown(button);
public bool IsMousePressed(FishMouseButton button) => ReadMousePressed(button);
public bool IsMouseReleased(FishMouseButton button) => ReadMouseReleased(button);
public FishTouchPoint[] GetTouchPoints() => Array.Empty<FishTouchPoint>();
}GetKeyPressed and GetCharPressed are consumptive queue methods. Return one item per call and return zero when the queue is empty. FishUI drains each queue once per update.
Return stable touch IDs with Press, Motion, and Release records. Return Array.Empty<FishTouchPoint>() when no touch exists.
Store native objects in immutable ImageRef and FontRef handles. The backend owns these objects. Release each native resource exactly once when the backend is disposed.
Texture2D tex = (Texture2D)Img.Userdata;FishUI does not dispose an injected backend. The application must dispose FishUI first and then dispose the backend.
Override BeginDrawing and EndDrawing if your graphics library needs frame begin/end calls:
public override void BeginDrawing(float Dt)
{
MyGraphics.BeginFrame();
MyGraphics.Clear(Color.White);
}
public override void EndDrawing()
{
MyGraphics.EndFrame();
MyGraphics.SwapBuffers();
}Call FishUI.Resized(width, height) when the window is resized:
if (WindowWasResized())
{
fishUI.Resized(GetWindowWidth(), GetWindowHeight());
}The default 9-patch implementation makes 9 draw calls. If your graphics library has native 9-slice support, override DrawNPatch for better performance.
The FishUI theme uses a texture atlas. Make sure your image drawing correctly handles the full image (not sub-regions) as the theme loader creates NPatch objects that reference specific atlas regions.
- IFishUIGfx.cs - Full interface definition
- SimpleFishUIGfx.cs - Base class with defaults
- RaylibFishGfx.cs - Complete Raylib example
- RaylibInput.cs - Raylib input example