Skip to content

TilePanelWidget ​

An infinite, pannable and zoomable tile map that renders content from a pluggable data source.

TilePanel displays a scrollable grid of tiles fetched from an ITileDataSource. Users navigate with arrow keys, zoom with +/- or mouse scroll, and can interact with clickable points of interest overlaid on the map. It follows a controlled component pattern — you own the camera state and update it in response to events.

Basic Usage ​

Create a TilePanel by providing an ITileDataSource and wiring up pan/zoom handlers:

csharp
using Hex1b;
using Hex1b.Data;
using Hex1b.Layout;
using Hex1b.Theming;
using Hex1b.Widgets;

var state = new MapState();
var dataSource = new GridTileDataSource();

await using var terminal = Hex1bTerminal.CreateBuilder()
    .WithHex1bApp((app, options) => ctx => ctx.VStack(v => [
        v.Text($"Camera: ({state.CameraX:F1}, {state.CameraY:F1})  Zoom: {state.ZoomLevel}"),
        v.TilePanel(dataSource, state.CameraX, state.CameraY, state.ZoomLevel)
            .OnPan(e =>
            {
                state.CameraX += e.DeltaX;
                state.CameraY += e.DeltaY;
            })
            .OnZoom(e => state.ZoomLevel = e.NewZoomLevel)
    ]))
    .Build();

await terminal.RunAsync();

class MapState
{
    public double CameraX { get; set; }
    public double CameraY { get; set; }
    public int ZoomLevel { get; set; }
}

class GridTileDataSource : ITileDataSource
{
    public Size TileSize => new(3, 1);

    public ValueTask<TileData[,]> GetTilesAsync(
        int tileX, int tileY, int tilesWide, int tilesTall,
        CancellationToken cancellationToken = default)
    {
        var tiles = new TileData[tilesWide, tilesTall];
        for (int y = 0; y < tilesTall; y++)
        {
            for (int x = 0; x < tilesWide; x++)
            {
                var tx = tileX + x;
                var ty = tileY + y;
                var isEven = (tx + ty) % 2 == 0;
                tiles[x, y] = new TileData(
                    FormatCoord(tx, ty),
                    isEven ? Hex1bColor.FromRgb(100, 180, 255) : Hex1bColor.FromRgb(180, 180, 180),
                    isEven ? Hex1bColor.FromRgb(20, 40, 80) : Hex1bColor.FromRgb(30, 50, 30));
            }
        }
        return ValueTask.FromResult(tiles);
    }

    static string FormatCoord(int x, int y)
    {
        var s = $"{x},{y}";
        return s.Length <= 3 ? s.PadRight(3) : s[..3];
    }
}

Navigation

Use arrow keys to pan, +/- to zoom, and Home to reset. Mouse drag pans the map, and scroll wheel zooms toward the cursor position.

Creating a Data Source ​

TilePanel gets its content from an ITileDataSource implementation. The interface has two members:

  • TileSize — the dimensions of each tile in characters at zoom level 0
  • GetTilesAsync() — fetches a rectangular region of tiles

Here's a minimal data source that renders a coordinate grid:

csharp

Async Loading

GetTilesAsync is called on a background thread, never blocking the UI. While tiles load, empty placeholders are shown. When tiles arrive, the panel automatically redraws.

TileData ​

Each tile is represented by a TileData record struct:

csharp
public readonly record struct TileData(
    string Content,       // Text content to render
    Hex1bColor Foreground, // Foreground color
    Hex1bColor Background  // Background color
);

The Content string length should match TileSize.Width. When zooming, content is automatically scaled to fill the effective tile width.

Camera Control ​

TilePanel uses a controlled component pattern. You own the camera state and update it via event handlers:

csharp
var cameraX = 0.0;
var cameraY = 0.0;
var zoom = 0;

ctx.TilePanel(dataSource, cameraX, cameraY, zoom)
    .OnPan(e =>
    {
        cameraX += e.DeltaX;
        cameraY += e.DeltaY;
    })
    .OnZoom(e => zoom = e.NewZoomLevel)

Pan Events ​

OnPan fires when the user presses arrow keys or drags the mouse. The TilePanelPanEventArgs provides:

PropertyTypeDescription
DeltaXdoubleHorizontal pan delta in tile units
DeltaYdoubleVertical pan delta in tile units

Arrow keys produce integer deltas (1 for normal, 5 for Shift+Arrow). Mouse drag produces fractional deltas based on pixel distance and current zoom.

Zoom Events ​

OnZoom fires when the user presses +/- keys or scrolls the mouse wheel. The TilePanelZoomEventArgs provides:

PropertyTypeDescription
NewZoomLevelintThe suggested zoom level after the change
DeltaintThe zoom delta (+1 or -1)
PivotXdoubleTile-space X coordinate of the zoom pivot
PivotYdoubleTile-space Y coordinate of the zoom pivot

For mouse scroll, the pivot is at the cursor position — useful for implementing zoom-toward-cursor:

csharp
.OnZoom(e =>
{
    // Adjust camera to keep the pivot point stable
    var scale = Math.Pow(2, e.Delta);
    cameraX = e.PivotX + (cameraX - e.PivotX) / scale;
    cameraY = e.PivotY + (cameraY - e.PivotY) / scale;
    zoom = e.NewZoomLevel;
})

Zoom Levels ​

Each zoom level doubles or halves the tile render size:

ZoomLevelScaleEffect
-20.25xQuarter size tiles
-10.5xHalf size tiles
01xBase size (from TileSize)
12xDouble size tiles
24xQuadruple size tiles

Zoom is internally clamped to the range -4 to 8.

Points of Interest ​

Overlay clickable markers on the tile map using PointsOfInterest:

csharp
var pois = new List<TilePointOfInterest>
{
    new(0, 0, "📍", "Origin"),           // X, Y, Icon, Label
    new(5, 3, "🏠", "Home", myData),     // Optional Tag for user data
};

ctx.TilePanel(dataSource, cameraX, cameraY, zoom)
    .PointsOfInterest(pois)
    .OnPoiClicked(e =>
    {
        // Navigate to the clicked POI
        cameraX = e.PointOfInterest.X;
        cameraY = e.PointOfInterest.Y;
    })

POIs outside the visible viewport are automatically excluded from rendering. The TilePointOfInterest record provides:

PropertyTypeDescription
XdoubleX coordinate in tile space
YdoubleY coordinate in tile space
IconstringIcon character or emoji to display
Labelstring?Optional text label near the icon
Tagobject?Optional user data

Input Bindings ​

TilePanel registers these default keybindings, all rebindable via InputBindings:

InputActionActionId
Arrow keysPan by 1 tilePanUp, PanDown, PanLeft, PanRight
Shift+Arrow keysPan by 5 tilesPanUpFast, PanDownFast, PanLeftFast, PanRightFast
+ / =Zoom inZoomIn
-Zoom outZoomOut
HomeReset to originResetPosition
Mouse scrollZoom at cursorZoomIn / ZoomOut
Mouse dragPan(drag handler)

Rebinding Keys ​

csharp
using Hex1b.Input;
using Hex1b.Widgets;

ctx.TilePanel(dataSource, cameraX, cameraY, zoom)
    .OnPan(e => { /* ... */ })
    .OnZoom(e => { /* ... */ })
    .InputBindings(b =>
    {
        // Use WASD instead of arrow keys
        b.Remove(TilePanelWidget.PanUp);
        b.Remove(TilePanelWidget.PanDown);
        b.Remove(TilePanelWidget.PanLeft);
        b.Remove(TilePanelWidget.PanRight);
        b.Key(Hex1bKey.W).Triggers(TilePanelWidget.PanUp);
        b.Key(Hex1bKey.S).Triggers(TilePanelWidget.PanDown);
        b.Key(Hex1bKey.A).Triggers(TilePanelWidget.PanLeft);
        b.Key(Hex1bKey.D).Triggers(TilePanelWidget.PanRight);
    })

Theming ​

Customize the appearance of empty tiles and POI labels:

csharp
using Hex1b.Theming;

var theme = new Hex1bTheme("Custom")
    .Set(TilePanelTheme.EmptyTileForegroundColor, Hex1bColor.DarkGray)
    .Set(TilePanelTheme.EmptyTileBackgroundColor, Hex1bColor.Black)
    .Set(TilePanelTheme.EmptyTileCharacter, '.')
    .Set(TilePanelTheme.PoiLabelForegroundColor, Hex1bColor.Yellow)
    .Set(TilePanelTheme.PoiLabelBackgroundColor, Hex1bColor.FromRgb(40, 40, 40));

Available Theme Elements ​

ElementTypeDefaultDescription
EmptyTileForegroundColorHex1bColorDarkGrayForeground for tiles with no data
EmptyTileBackgroundColorHex1bColorDefaultBackground for tiles with no data
EmptyTileCharacterchar·Character used to fill empty tiles
PoiLabelForegroundColorHex1bColorWhitePOI label text color
PoiLabelBackgroundColorHex1bColorDefaultPOI label background color

Async Loading ​

TilePanel loads tiles asynchronously — GetTilesAsync is called on a background thread, never blocking the UI. While tiles load, empty placeholders are shown. When tiles arrive, the panel automatically redraws.

Data Source Caching

If your data source involves expensive I/O (e.g., network requests), implement caching within your ITileDataSource. The panel calls GetTilesAsync whenever the viewport changes, so a fast return path keeps the UI responsive.

Extension Methods ​

MethodDescription
ctx.TilePanel(dataSource)Creates a TilePanel at origin with zoom 0
ctx.TilePanel(dataSource, cameraX, cameraY, zoomLevel)Creates a TilePanel with specified camera position
  • Scroll - For scrollable content within fixed bounds
  • Surface - For custom low-level rendering
  • Float - For absolute positioning of overlays

Released under the MIT License.