< Summary

Information
Class: Chronicis.Client.Components.Articles.DrawingCanvas
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Articles/DrawingCanvas.razor
Line coverage
100%
Covered lines: 10
Uncovered lines: 0
Coverable lines: 10
Total lines: 217
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor()100%11100%
OnStrokeCountChanged(...)100%11100%
GetColorButtonStyle(...)100%22100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Articles/DrawingCanvas.razor

#LineLine coverage
 1@* DrawingCanvas.razor - HTML5 Canvas drawing surface with stylus/touch/mouse input *@
 2@using Microsoft.JSInterop
 3@implements IAsyncDisposable
 4@inject IJSRuntime JSRuntime
 5
 6<div class="drawing-canvas-container">
 7    @* Toolbar *@
 8    <MudPaper Class="drawing-canvas-toolbar pa-2 mb-2" Elevation="1">
 9        <div class="d-flex align-center flex-wrap" style="gap: 8px;">
 10            @* Color Picker *@
 11            <MudButtonGroup OverrideStyles="false" Class="drawing-canvas-colors">
 12                @foreach (var color in _colors)
 13                {
 14                    <MudIconButton Icon="@Icons.Material.Filled.Circle"
 15                                   Size="Size.Small"
 16                                   Style="@GetColorButtonStyle(color)"
 17                                   OnClick="@(() => SelectColor(color))"
 18                                   aria-label="@($"Select color {color}")" />
 19                }
 20            </MudButtonGroup>
 21
 22            <MudDivider Vertical="true" FlexItem="true" Class="mx-1" />
 23
 24            @* Pen/Eraser Toggle *@
 25            <MudButtonGroup OverrideStyles="false">
 26                <MudIconButton Icon="@Icons.Material.Filled.Edit"
 27                               Size="Size.Small"
 28                               Color="@(_currentTool == "pen" ? Color.Primary : Color.Default)"
 29                               Variant="@(_currentTool == "pen" ? Variant.Filled : Variant.Outlined)"
 30                               OnClick="@(() => SelectTool("pen"))"
 31                               aria-label="Pen tool" />
 32                <MudIconButton Icon="@Icons.Material.Filled.AutoFixOff"
 33                               Size="Size.Small"
 34                               Color="@(_currentTool == "eraser" ? Color.Primary : Color.Default)"
 35                               Variant="@(_currentTool == "eraser" ? Variant.Filled : Variant.Outlined)"
 36                               OnClick="@(() => SelectTool("eraser"))"
 37                               aria-label="Eraser tool" />
 38            </MudButtonGroup>
 39
 40            <MudDivider Vertical="true" FlexItem="true" Class="mx-1" />
 41
 42            @* Undo/Redo *@
 43            <MudButtonGroup OverrideStyles="false">
 44                <MudIconButton Icon="@Icons.Material.Filled.Undo"
 45                               Size="Size.Small"
 46                               OnClick="Undo"
 47                               aria-label="Undo" />
 48                <MudIconButton Icon="@Icons.Material.Filled.Redo"
 49                               Size="Size.Small"
 50                               OnClick="Redo"
 51                               aria-label="Redo" />
 52            </MudButtonGroup>
 53
 54            <MudSpacer />
 55
 56            @* Save & Transcribe *@
 57            <MudButton Variant="Variant.Outlined"
 58                       Color="Color.Primary"
 59                       StartIcon="@Icons.Material.Filled.Save"
 60                       OnClick="HandleSave"
 61                       Disabled="@(_strokeCount == 0 || IsSaving)">
 62                Save
 63            </MudButton>
 64            <MudButton Variant="Variant.Filled"
 65                       Color="Color.Primary"
 66                       StartIcon="@Icons.Material.Filled.Transcribe"
 67                       OnClick="HandleTranscribe"
 68                       Disabled="@(_strokeCount == 0 || IsSaving)">
 69                Transcribe
 70            </MudButton>
 71        </div>
 72    </MudPaper>
 73
 74    @* Canvas *@
 75    <div class="drawing-canvas-scroll-container" style="overflow-y: auto;">
 76        <canvas id="@_canvasElementId"
 77                width="2000"
 78                height="2000"
 79                style="width: 100%; min-height: 2000px; display: block; border: 1px solid var(--mud-palette-lines-defaul
 80        </canvas>
 81    </div>
 82</div>
 83
 84@code {
 85    private const string CanvasIdPrefix = "drawing-canvas-";
 86
 187    private static readonly string[] _colors = { "#000000", "#FF0000", "#0000FF", "#008000", "#FF8C00", "#800080" };
 88
 2989    private string _canvasElementId = $"{CanvasIdPrefix}{Guid.NewGuid():N}";
 2990    private string _currentColor = _colors[0];
 2991    private string _currentTool = "pen";
 92    private int _strokeCount;
 93    private DotNetObjectReference<DrawingCanvas>? _dotNetRef;
 94    private bool _initialized;
 95
 96    #region Parameters
 97
 98    /// <summary>
 99    /// Invoked with PNG bytes when user clicks Save.
 100    /// </summary>
 101    [Parameter]
 102    public EventCallback<byte[]> OnSave { get; set; }
 103
 104    /// <summary>
 105    /// Invoked with PNG bytes when user clicks Transcribe.
 106    /// </summary>
 107    [Parameter]
 108    public EventCallback<byte[]> OnTranscribe { get; set; }
 109
 110    /// <summary>
 111    /// Disables save/transcribe buttons during operation.
 112    /// </summary>
 113    [Parameter]
 114    public bool IsSaving { get; set; }
 115
 116    #endregion
 117
 118    protected override async Task OnAfterRenderAsync(bool firstRender)
 119    {
 120        if (firstRender)
 121        {
 122            _dotNetRef = DotNetObjectReference.Create(this);
 123            _initialized = await JSRuntime.InvokeAsync<bool>(
 124                "drawingCanvasInitialize", _canvasElementId, _dotNetRef);
 125        }
 126    }
 127
 128    /// <summary>
 129    /// Called from JS when stroke count changes.
 130    /// </summary>
 131    [JSInvokable]
 132    public void OnStrokeCountChanged(int strokeCount)
 133    {
 11134        _strokeCount = strokeCount;
 11135        StateHasChanged();
 11136    }
 137
 138    private async Task SelectColor(string color)
 139    {
 140        _currentColor = color;
 141        if (_initialized)
 142        {
 143            await JSRuntime.InvokeVoidAsync("drawingCanvasSetColor", _canvasElementId, color);
 144        }
 145    }
 146
 147    private async Task SelectTool(string tool)
 148    {
 149        _currentTool = tool;
 150        if (_initialized)
 151        {
 152            await JSRuntime.InvokeVoidAsync("drawingCanvasSetTool", _canvasElementId, tool);
 153        }
 154    }
 155
 156    private async Task Undo()
 157    {
 158        if (_initialized)
 159        {
 160            await JSRuntime.InvokeVoidAsync("drawingCanvasUndo", _canvasElementId);
 161        }
 162    }
 163
 164    private async Task Redo()
 165    {
 166        if (_initialized)
 167        {
 168            await JSRuntime.InvokeVoidAsync("drawingCanvasRedo", _canvasElementId);
 169        }
 170    }
 171
 172    private async Task HandleSave()
 173    {
 174        if (!_initialized || _strokeCount == 0) return;
 175
 176        var pngBytes = await JSRuntime.InvokeAsync<byte[]>("drawingCanvasExportPng", _canvasElementId);
 177        if (pngBytes != null)
 178        {
 179            await OnSave.InvokeAsync(pngBytes);
 180        }
 181    }
 182
 183    private async Task HandleTranscribe()
 184    {
 185        if (!_initialized || _strokeCount == 0) return;
 186
 187        var pngBytes = await JSRuntime.InvokeAsync<byte[]>("drawingCanvasExportPng", _canvasElementId);
 188        if (pngBytes != null)
 189        {
 190            await OnTranscribe.InvokeAsync(pngBytes);
 191        }
 192    }
 193
 194    private string GetColorButtonStyle(string color)
 195    {
 242196        var selected = color == _currentColor;
 242197        var border = selected ? "3px solid var(--mud-palette-primary)" : "2px solid transparent";
 242198        return $"color: {color}; border: {border}; border-radius: 50%;";
 199    }
 200
 201    public async ValueTask DisposeAsync()
 202    {
 203        if (_initialized)
 204        {
 205            try
 206            {
 207                await JSRuntime.InvokeVoidAsync("drawingCanvasDispose", _canvasElementId);
 208            }
 209            catch (JSDisconnectedException)
 210            {
 211                // Circuit disconnected, nothing to clean up on JS side
 212            }
 213        }
 214
 215        _dotNetRef?.Dispose();
 216    }
 217}