Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/SharpClient.App/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@ public MainPage()
// status-bar/cutout insets and the soft-keyboard height reach the HTML layer even on Android
// System WebViews too old for CSS env()/visualViewport (the device was on WebView 133).
blazorWebView.BlazorWebViewInitialized += (_, e) =>
{
SharpClient.App.Platforms.Android.WebViewInsetsBridge.Attach(e.WebView);

// Android WebView clamps CSS font-size to a MinimumFontSize (default 8px) and
// MinimumLogicalFontSize (default 8px). The terminal fits the font down to a few px to pack
// a high "Min columns" onto a narrow screen; the clamp silently rendered those at 8px, so
// e.g. 90 columns never fit and NAWS stayed pinned at ~78. Drop the floor so the fitted size
// is actually honoured. (measureGrid still measures the real rendered advance and reports
// only the columns that fit, so nothing wraps if a value is genuinely too large.)
if (e.WebView.Settings is { } settings)
{
settings.MinimumFontSize = 1;
settings.MinimumLogicalFontSize = 1;
}
};
#endif
}
}
33 changes: 31 additions & 2 deletions src/SharpClient.UI/Components/SessionScreen.razor
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
@using Microsoft.Extensions.Logging
@using Microsoft.JSInterop
@using SharpClient.Core.Connection
@using SharpClient.Core.Presentation
@using SharpClient.Core.Sessions

@inject SettingsViewModel Settings
@inject IJSRuntime JS
@inject ILogger<SessionScreen> Logger

@implements IAsyncDisposable

Expand Down Expand Up @@ -110,7 +112,24 @@
// which had wrapped an advertised 78 columns ~2 short. measureGrid applies --out-fs (fitted
// size) and --sc-cols (column track width) on the element.
var cap = Math.Max(6, Settings.MaxFontSize);
var grid = await _interop.InvokeAsync<GridDims>("measureGrid", _outputRef, Settings.MinColumns, 6, cap);
// Min font 4px (not 6): a high MinColumns on a narrow phone needs a small font to fit — e.g. 90
// columns in ~376px needs ~5.9px. Flooring at 6px stopped short, so 90 wrapped. measureGrid
// reports the honest fitted column count, so if even 4px can't fit MinColumns it advertises what
// does fit instead of over-advertising.
const int minFont = 4;
var grid = await _interop.InvokeAsync<GridDims>("measureGrid", _outputRef, Settings.MinColumns, minFont, cap);

// Diagnostics for column/wrap issues — captured to the on-device log (FileLoggerProvider) so it
// can be exported. actualCols < targetCols means the advertised width can't be shown and lines
// wrap; compare fontPx against the 6px floor and contentW against clientW (scrollbar/inset loss).
Logger.LogInformation(
"NAWS fit: minCols(setting)={MinColumns} maxFont(cap)={Cap} probeW={ProbeWidth} probeH={ProbeHeight} " +
"clientW={ClientW} contentW={ContentW} padX={PadX} advanceRatio={AdvanceRatio} fontPx={FontPx} " +
"targetCols={TargetCols} actualColsThatFit={ActualCols} reportedCols(NAWS)={Cols} rows={Rows} vScroll={VScroll} fits={Fits}",
Settings.MinColumns, cap, widthPx, heightPx,
grid.ClientW, grid.ContentW, grid.PadX, grid.AdvanceRatio, grid.FontPx,
grid.TargetCols, grid.ActualCols, grid.Cols, grid.Rows, grid.VScroll,
grid.ActualCols >= grid.TargetCols);
Comment on lines +122 to +132

if (Vm.Active is not null)
{
Expand Down Expand Up @@ -144,5 +163,15 @@

private sealed record SizeDims(int Width, int Height);

private sealed record GridDims(int Cols, int Rows);
private sealed record GridDims(
int Cols,
int Rows,
double FontPx,
double ClientW,
double ContentW,
double PadX,
double AdvanceRatio,
int TargetCols,
int ActualCols,
bool VScroll);
}
29 changes: 24 additions & 5 deletions src/SharpClient.UI/Components/SettingsView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@
<div class="sc-settings-section-header">Layout</div>
<div class="sc-setting">
<span class="sc-setting-label">Min columns</span>
<div class="sc-slider-row">
<input type="range" class="sc-slider"
min="60" max="120" step="1"
<div class="sc-stepper">
<button type="button" class="sc-stepper-btn" aria-label="Fewer columns"
@onclick="() => AdjustMinColumns(-1)">−</button>
<input type="number" inputmode="numeric" class="sc-stepper-input"
min="@MinColumnsMin" max="@MinColumnsMax" step="1"
value="@Vm.MinColumns"
@onchange="e => Vm.MinColumns = int.Parse(e.Value!.ToString()!)" />
<span class="sc-setting-value">@Vm.MinColumns</span>
@onchange="OnMinColumnsInput" />
<button type="button" class="sc-stepper-btn" aria-label="More columns"
@onclick="() => AdjustMinColumns(1)">+</button>
</div>
</div>
<div class="sc-setting">
Expand Down Expand Up @@ -103,6 +106,22 @@

private bool _exporting;

// Typeable/stepped bounds for Min columns. Wider than the old 60–120 slider so unusual widths
// are reachable; the value is clamped on every change.
private const int MinColumnsMin = 20;
Comment on lines +109 to +111
private const int MinColumnsMax = 500;

private void AdjustMinColumns(int delta) =>
Vm.MinColumns = Math.Clamp(Vm.MinColumns + delta, MinColumnsMin, MinColumnsMax);

private void OnMinColumnsInput(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out var value))
{
Vm.MinColumns = Math.Clamp(value, MinColumnsMin, MinColumnsMax);
}
}

protected override void OnInitialized() => Vm.Changed += OnChanged;

private void OnChanged() => InvokeAsync(StateHasChanged);
Expand Down
8 changes: 8 additions & 0 deletions src/SharpClient.UI/wwwroot/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,14 @@ body {
.sc-slider-row { display: flex; align-items: center; gap: 10px; }
.sc-slider { flex: 1; accent-color: var(--acc); cursor: pointer; }

/* Stepper (− value +) — easier to set an exact value than a slider */
.sc-stepper { display: inline-flex; align-items: stretch; gap: 0; border: 1px solid var(--bd2); border-radius: 9px; overflow: hidden; background: var(--elev); }
.sc-stepper-btn { width: 38px; border: none; background: transparent; color: var(--acc2); font-family: var(--ui); font-size: 20px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.sc-stepper-btn:active { background: var(--acc-soft); }
.sc-stepper-input { width: 56px; text-align: center; border: none; border-left: 1px solid var(--bd2); border-right: 1px solid var(--bd2); background: var(--panel); color: var(--tx); font-family: var(--mono); font-size: 14px; padding: 8px 4px; -moz-appearance: textfield; appearance: textfield; }
.sc-stepper-input::-webkit-outer-spin-button,
.sc-stepper-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Toggle */
.sc-toggle { width: 36px; height: 20px; appearance: none; background: var(--elev); border-radius: 10px; position: relative; cursor: pointer; border: 1px solid var(--bd2); transition: background .15s; }
.sc-toggle::before { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--dim); transition: transform .15s, background .15s; }
Expand Down
35 changes: 29 additions & 6 deletions src/SharpClient.UI/wwwroot/sc-interop.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ export function observeResize(dotNetRef, element) {
* sub-pixel rounding), never derived from a hard-coded 0.6em — that guess made an advertised 78
* columns wrap ~2 short. A closed loop corrects for glyph-advance non-linearity (hinting): after the
* ideal size is computed it re-measures `targetCols` chars and shrinks if they overflow. Font growth
* is capped at `maxFont` (line-length cap; content left-aligns), floored at `minFont` (below that the
* caller's --sc-cols track scrolls horizontally rather than rendering illegibly small).
* is capped at `maxFont` (line-length cap; content left-aligns), floored at `minFont`. If even at
* `minFont` the targetCols don't fit, the RETURNED column count is the number that actually fits (not
* targetCols), so NAWS never advertises more than is visible and lines don't wrap on the client.
*
* Ported from SharpMUSH.Client's terminalMetrics.js so the two clients agree on column math.
* Applies the fitted size as --out-fs and the column count as --sc-cols on the element, and returns
Expand Down Expand Up @@ -139,7 +140,6 @@ export function measureGrid(element, targetCols, minFont, maxFont) {
const contentH = element.clientHeight - padY;

let fontPx;
let cols;
if (targetCols > 0) {
const targetW = contentW - 1; // a hair inside the box (avoid a sub-pixel scrollbar)
let fit = targetW / targetCols / advanceRatio;
Expand All @@ -158,16 +158,39 @@ export function measureGrid(element, targetCols, minFont, maxFont) {
fit = Math.max(minF, fit * (targetW / actual));
}
fontPx = fit;
cols = targetCols; // honour the preferred width; the --sc-cols track scrolls if it overflows
} else {
fontPx = parseFloat(cs.getPropertyValue('--out-fs')) || 14;
cols = clampGrid(Math.floor(contentW / (advanceRatio * fontPx)));
}

// Columns that ACTUALLY fit — measured with the REAL glyph advance at the fitted size, NOT the
// linear advanceRatio (sampled at 100px). Glyph advance is proportionally larger at tiny sizes
// (hinting/rounding), so the linear value over-counts near the min-font floor: it once reported 104
// columns "fit" at 6px when only ~78 truly did, so an advertised 90 wrapped. Reporting the honest
// count means NAWS never advertises more than is visible, so the server wraps to fit and the client
// never wraps a second time.
const realAdvance = runWidth(fontPx, '0'.repeat(100)) / 100;
const fitCols = realAdvance > 0 ? Math.floor(contentW / realAdvance) : 1;
const cols = targetCols > 0
? Math.min(targetCols, Math.max(1, fitCols))
: clampGrid(fitCols);
const vscroll = element.scrollHeight > element.clientHeight;

element.style.setProperty('--out-fs', fontPx + 'px');
element.style.setProperty('--sc-cols', String(cols));

return { cols, rows: clampGrid(Math.floor(contentH / (lineRatio * fontPx))) };
return {
cols,
rows: clampGrid(Math.floor(contentH / (lineRatio * fontPx))),
// diagnostics (see SessionScreen logging)
fontPx: Math.round(fontPx * 100) / 100,
clientW: element.clientWidth,
contentW,
padX,
advanceRatio: Math.round(advanceRatio * 10000) / 10000,
targetCols,
actualCols: fitCols,
vscroll,
};
}

/**
Expand Down
25 changes: 21 additions & 4 deletions tests/SharpClient.UI.Tests/SettingsViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,34 @@ public async Task ClickingFontOptionUpdatesVmFont()
}

[Test]
public async Task MinColumnsSliderRendersWithCorrectValue()
public async Task MinColumnsStepperRendersWithCorrectValue()
{
using var ctx = NewContext();
var vm = MakeVm();
vm.MinColumns = 90;

var cut = ctx.Render<SettingsView>(p => p.Add(c => c.Vm, vm));

// The slider for MinColumns should have value="90"
var slider = cut.Find("input[type='range'][min='60']");
await Assert.That(slider.GetAttribute("value")).IsEqualTo("90");
// Min columns is now a typeable number stepper (not a slider).
var input = cut.Find("input.sc-stepper-input");
await Assert.That(input.GetAttribute("value")).IsEqualTo("90");
}

[Test]
public async Task MinColumnsStepperButtonsAdjustValue()
{
using var ctx = NewContext();
var vm = MakeVm();
vm.MinColumns = 90;

var cut = ctx.Render<SettingsView>(p => p.Add(c => c.Vm, vm));

var buttons = cut.FindAll(".sc-stepper-btn");
buttons[1].Click(); // "+"
await Assert.That(vm.MinColumns).IsEqualTo(91);
buttons[0].Click(); // "−"
buttons[0].Click();
await Assert.That(vm.MinColumns).IsEqualTo(89);
}

[Test]
Expand Down
Loading