Public Access
Merge pull request 'Type a path on the SFTP pane, and put a file right without deleting it' (#15) from claude/dodossh-sftp-improvements-3b6305 into main
Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
commit
1970e20e56
2 files changed
+350
-13
No files matched your search
@@ -33,6 +33,43 @@
|
||||
layer has not got. See docs/design-import-gaps.md.
|
||||
-->
|
||||
|
||||
<!--
|
||||
══ THE TWO LISTINGS' COLUMNS ═════════════════════════════════════════════════════════════════════════
|
||||
◆ They were fixed pixel widths, and fixed was wrong at both ends of the range this screen actually gets.
|
||||
|
||||
The numbers — 20/*/60/56 here and 20/*/56/52/48 across the arrow column — were chosen against the 204
|
||||
pixels the session shell's own QUICK ACCESS sidebar leaves a pane at the window's minimum. Measured,
|
||||
they were wrong in both directions at once:
|
||||
|
||||
· MODIFIED prints "2026-08-21 11:36", which needs 116 pixels, and never had more than 48. So the
|
||||
column was truncated at every width this screen has ever been drawn at — a maximised window showed
|
||||
"2026-08" beside a NAME column with four hundred empty pixels in it. PERMS needs 60 for a
|
||||
ten-character mode and had 48; SIZE needs 58 for "999.9 GB" and had 56.
|
||||
· At 204 the remote pane's NAME column measured **zero pixels wide**. Twenty for the glyph plus
|
||||
56+52+48 for the numbers is 176 of the 180 a row has, so the file names — the one column a file
|
||||
browser exists to show — were not drawn at all.
|
||||
|
||||
Star columns with a MaxWidth fix both, because the cap is what makes "share the width" mean the right
|
||||
thing at each end. Wide, the three numeric columns stop growing at what their own text needs and NAME
|
||||
keeps every remaining pixel. Narrow, nothing has a MinWidth to overflow with, so all four give ground
|
||||
together and NAME — weighted heaviest — keeps enough to read a name by.
|
||||
|
||||
MODIFIED carries the extra weight (1.6) so that it reaches its cap first: it is the widest content and
|
||||
the column somebody is most often actually reading. The local pane's NAME is 3* against the remote's 2*,
|
||||
which is the remote's PERMS weight folded into it — that is what makes SIZE and MODIFIED come out the
|
||||
same width on both sides of the arrow column, so the same amount of a timestamp is visible in each. See
|
||||
Timestamps.Format for why both panes write one the same way in the first place; a shared format read at
|
||||
two different widths would have made that argument for nothing.
|
||||
|
||||
◆ FOUR COPIES OF THIS, and they have to stay in step: a heading row and a row template per pane. Avalonia
|
||||
cannot share one ColumnDefinitions instance between two Grids — each mutates its own computed sizes —
|
||||
and SharedSizeGroup only applies to Auto columns. What catches a copy that drifts is measurement rather
|
||||
than discipline: ScreenLayoutTests holds every cell against the width its own text needs
|
||||
(TheListingColumnsShowTheirContentInFullWhenThereIsRoom), the headings against each other
|
||||
(TheListingHeadingsDoNotRunIntoEachOther), and NAME against zero at the narrowest budget
|
||||
(TheNameColumnSurvivesTheNarrowestBudget).
|
||||
-->
|
||||
|
||||
<UserControl.Styles>
|
||||
<!--
|
||||
── v5b ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
@@ -259,10 +296,18 @@
|
||||
the row template below for why the whole column set was chosen against 204 pixels rather than the
|
||||
design's own 1920-pixel canvas.
|
||||
-->
|
||||
<Grid Grid.Row="2" ColumnDefinitions="20,*,60,56" Margin="0,2,10,4">
|
||||
<Grid Grid.Row="2" Margin="0,2,10,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="3*" />
|
||||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||||
<ColumnDefinition Width="1.6*" MaxWidth="124" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="8,0,8,0" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" HorizontalAlignment="Right" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" HorizontalAlignment="Right" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" Margin="8,0,0,0"
|
||||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" Margin="8,0,0,0"
|
||||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
@@ -274,13 +319,19 @@
|
||||
SelectedItem="{Binding SelectedLocalEntry}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:LocalEntryRowViewModel">
|
||||
<Grid ColumnDefinitions="20,*,60,56" Height="38" Margin="14,0,10,0">
|
||||
<Grid Height="38" Margin="14,0,10,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="3*" />
|
||||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||||
<ColumnDefinition Width="1.6*" MaxWidth="124" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Classes="entryicon" Text="{Binding IconGlyph}" />
|
||||
<TextBlock Grid.Column="1" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
|
||||
Text="{Binding Name}" FontSize="13.5"
|
||||
Margin="10,0,8,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="2" Classes="mono entrysize" Text="{Binding Size}" FontSize="12"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||
Margin="8,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono entrydate" Text="{Binding Modified}" FontSize="12"
|
||||
Margin="8,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
@@ -374,8 +425,14 @@
|
||||
</StackPanel>
|
||||
<TextBox Grid.Column="1" Classes="pathbox" VerticalAlignment="Center"
|
||||
Text="{Binding RemotePathDraft}" IsEnabled="{Binding IsConnected}"
|
||||
PlaceholderText="/path/to/somewhere"
|
||||
PlaceholderText="path"
|
||||
ToolTip.Tip="Type a path and press Enter. A relative path is taken from here; ~ is the account's home directory.">
|
||||
<!--
|
||||
One word, because the only time this placeholder shows is on a pane with nothing connected,
|
||||
where the box is disabled and the invitation below is doing the explaining. "/path/to/
|
||||
somewhere" needs 146 pixels and had 91 at the session shell's narrowest, so it drew as a
|
||||
path cut off mid-segment. What the box is for is in the tooltip, where there is room for it.
|
||||
-->
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding GoToTypedRemotePathCommand}" />
|
||||
<KeyBinding Gesture="Escape" Command="{Binding RevertTypedRemotePathCommand}" />
|
||||
@@ -584,10 +641,16 @@
|
||||
is showing, 140 plus MKDIR plus the spacing between them ran past the pane's own edge. Still wide
|
||||
enough for a real directory name to be typed and read back, which is what this box is for; a
|
||||
name that outgrows it scrolls inside the box the way every TextBox already does.
|
||||
|
||||
◆ The placeholder says "directory" rather than "new directory", which is what it said until it
|
||||
was measured: the longer text needs 101 pixels inside a box that has 74, so it drew as "new
|
||||
directo" — a placeholder cut off mid-word, which reads as a rendering fault rather than as a
|
||||
hint. MKDIR is right beside it and carries the "new" half already. The path box in the header
|
||||
above lost the same argument and for the same reason; see its own PlaceholderText.
|
||||
-->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
||||
IsVisible="{Binding IsConnected}">
|
||||
<TextBox Width="90" Text="{Binding NewRemoteFolder}" PlaceholderText="new directory" />
|
||||
<TextBox Width="90" Text="{Binding NewRemoteFolder}" PlaceholderText="directory" />
|
||||
<Button Classes="ghost" Content="MKDIR" Command="{Binding CreateRemoteFolderCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -597,25 +660,42 @@
|
||||
48 pixels rather than the drawer's own wider habit, and SIZE/MODIFIED shrink to match the local
|
||||
pane's narrower numbers. See the local pane's own remark on why 204 rather than the design's 1920.
|
||||
-->
|
||||
<Grid Grid.Row="5" ColumnDefinitions="20,*,56,52,48" Margin="0,2,10,4">
|
||||
<Grid Grid.Row="5" Margin="0,2,10,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="2*" />
|
||||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||||
<ColumnDefinition Width="1.6*" MaxWidth="124" />
|
||||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="8,0,8,0" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" HorizontalAlignment="Right" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" HorizontalAlignment="Right" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="9.5" HorizontalAlignment="Right" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" Margin="8,0,0,0"
|
||||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" Margin="8,0,0,0"
|
||||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="9.5" Margin="6,0,0,0"
|
||||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
|
||||
<ListBox Grid.Row="6" x:Name="RemoteList" Classes="filerows" ItemsSource="{Binding RemoteEntries}"
|
||||
SelectedItem="{Binding SelectedRemoteEntry}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:RemoteEntryRowViewModel">
|
||||
<Grid ColumnDefinitions="20,*,56,52,48" Height="38" Margin="14,0,10,0">
|
||||
<Grid Height="38" Margin="14,0,10,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="2*" />
|
||||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||||
<ColumnDefinition Width="1.6*" MaxWidth="124" />
|
||||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Classes="entryicon" Text="{Binding IconGlyph}" />
|
||||
<TextBlock Grid.Column="1" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
|
||||
Classes.exec="{Binding IsExecutable}"
|
||||
Text="{Binding Name}" FontSize="13.5"
|
||||
Margin="10,0,8,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="2" Classes="mono entrysize" Text="{Binding Size}" FontSize="12"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||
Margin="8,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono entrydate" Text="{Binding Modified}" FontSize="12"
|
||||
Margin="8,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="4" Classes="mono perms" Classes.loose="{Binding IsWorldWritable}"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Headless;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Threading;
|
||||
@@ -2171,6 +2172,262 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
/// runtime — see <see cref="LayoutHarness.SessionScreenWidth"/> — so a test that connects before calling
|
||||
/// this measures the tighter, sidebar-narrowed shape without having to say so twice.
|
||||
/// </remarks>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ---- ◆ The listings' columns ----
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Every cell shows what it holds, at a width this screen actually gets.</b> The columns were fixed
|
||||
/// pixel widths until they were measured against their own content: MODIFIED prints a sixteen-character
|
||||
/// timestamp needing 116 pixels and had 52, so it was truncated at every width the application has ever
|
||||
/// been drawn at — on a maximised window it read "2026-08" beside a NAME column with four hundred empty
|
||||
/// pixels in it. PERMS and SIZE were short by the same kind of margin.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Asserted by measuring each cell's own text unconstrained and holding the rendered width against it,
|
||||
/// rather than against numbers written down here: a column that fits is a column at least as wide as
|
||||
/// what it has to draw, and any other formulation is this test agreeing with the markup about an
|
||||
/// arithmetic they both got from the same place.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheListingColumnsShowTheirContentInFullWhenThereIsRoom()
|
||||
{
|
||||
SeedBothListings();
|
||||
|
||||
await LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
{
|
||||
var screen = new TransfersScreen { DataContext = transfers };
|
||||
var window = LayoutHarness.HostAtMinimumSize(
|
||||
screen, MaximisedSessionScreenWidth, LayoutHarness.SessionScreenHeight);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var text in new[] { "2.1 KB", "2026-08-21 11:36", "-rw-r--r--", "2026-07-04 09:00" })
|
||||
{
|
||||
var cell = screen.GetVisualDescendants()
|
||||
.OfType<TextBlock>()
|
||||
.First(block => string.Equals(block.Text, text, StringComparison.Ordinal));
|
||||
|
||||
cell.Bounds.Width.ShouldBeGreaterThanOrEqualTo(
|
||||
Unconstrained(cell) - 0.5, $"'{text}' has to fit the column it is drawn in");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
},
|
||||
Token);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The headings are right-aligned in their own columns, so two of them whose columns are exactly the
|
||||
/// width of their text run together: SIZE ended on the pixel MODIFIED began at, and the row read
|
||||
/// "SIZEMODIFIED". This holds the gap that fixes it — a left margin on each of the three, which does not
|
||||
/// move a right-aligned label but does stop the column before it reaching this one.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheListingHeadingsDoNotRunIntoEachOther()
|
||||
{
|
||||
SeedBothListings();
|
||||
|
||||
await LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
{
|
||||
var screen = new TransfersScreen { DataContext = transfers };
|
||||
var window = LayoutHarness.HostAtMinimumSize(
|
||||
screen, MaximisedSessionScreenWidth, LayoutHarness.SessionScreenHeight);
|
||||
|
||||
try
|
||||
{
|
||||
// Per pane, not across both. Each has its own NAME at its own left edge, and a list of
|
||||
// all seven sorted by position pairs the local pane's last heading with the remote
|
||||
// pane's first — two labels in different panes, which no amount of margin separates.
|
||||
var rows = screen.GetVisualDescendants()
|
||||
.OfType<Grid>()
|
||||
.Where(grid => grid.Children.OfType<TextBlock>().Any(
|
||||
block => block.Classes.Contains("label")
|
||||
&& string.Equals(block.Text, "NAME", StringComparison.Ordinal)))
|
||||
.ToList();
|
||||
|
||||
rows.Count.ShouldBe(2, "one heading row per pane");
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var headings = row.Children
|
||||
.OfType<TextBlock>()
|
||||
.Where(block => block.Classes.Contains("label"))
|
||||
.Select(block => (block.Text, Left: block.Bounds.X, Right: block.Bounds.Right))
|
||||
.OrderBy(heading => heading.Left)
|
||||
.ToList();
|
||||
|
||||
foreach (var (before, after) in headings.Zip(headings.Skip(1)))
|
||||
{
|
||||
after.Left.ShouldBeGreaterThan(
|
||||
before.Right, $"'{before.Text}' must not touch '{after.Text}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
},
|
||||
Token);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The other end of the same fault. At the 204 pixels the session shell's own sidebar leaves a pane, the
|
||||
/// fixed columns took 176 of a row's 180 and the remote pane's NAME cell measured <b>zero</b> — a file
|
||||
/// browser drawing sizes, timestamps and modes for rows whose names were not on screen at all.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// No column carries a MinWidth, which is what keeps this from being a fight the row can lose: at any
|
||||
/// width all four give ground together, and NAME — weighted heaviest — is the last to run out. What is
|
||||
/// asserted is only that it has not run out, because how much of a name fits in 204 pixels is a question
|
||||
/// with no good answer.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheNameColumnSurvivesTheNarrowestBudget()
|
||||
{
|
||||
SeedBothListings();
|
||||
|
||||
await LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
{
|
||||
var screen = new TransfersScreen { DataContext = transfers };
|
||||
var window = LayoutHarness.HostAtMinimumSize(
|
||||
screen, LayoutHarness.SessionScreenWidth(sidebarVisible: true), LayoutHarness.SessionScreenHeight);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var name in new[] { ".bash_history", ".azure-devops" })
|
||||
{
|
||||
var cell = screen.GetVisualDescendants()
|
||||
.OfType<TextBlock>()
|
||||
.First(block => string.Equals(block.Text, name, StringComparison.Ordinal));
|
||||
|
||||
cell.Bounds.Width.ShouldBeGreaterThan(20, $"'{name}' must be readable at all");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
},
|
||||
Token);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A placeholder cut off mid-word reads as a rendering fault rather than as a hint — MKDIR's box drew
|
||||
/// "new directo" and the pane header's drew a path ending mid-segment. Both are boxes whose width is
|
||||
/// fixed by the pane's own narrowest budget, so the text is what had to give.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ThePaneBoxesPlaceholdersFitTheBoxesTheyAreIn()
|
||||
{
|
||||
transfers.IsConnected = true;
|
||||
|
||||
await LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
{
|
||||
var screen = new TransfersScreen { DataContext = transfers };
|
||||
var window = LayoutHarness.HostAtMinimumSize(
|
||||
screen, LayoutHarness.SessionScreenWidth(sidebarVisible: true), LayoutHarness.SessionScreenHeight);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var box in screen.GetVisualDescendants().OfType<TextBox>())
|
||||
{
|
||||
// The picker's own password box is in the collapsed half of the invitation, so it
|
||||
// lays out at zero width and has no room for anything. Only what is on screen.
|
||||
if (box.Watermark is not { Length: > 0 } mark || !box.IsEffectivelyVisible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var room = box.Bounds.Width - box.Padding.Left - box.Padding.Right;
|
||||
|
||||
room.ShouldBeGreaterThanOrEqualTo(
|
||||
Measured(mark, box.FontSize, box.FontFamily),
|
||||
$"the placeholder '{mark}' has to fit its own box");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
},
|
||||
Token);
|
||||
}
|
||||
|
||||
/// <summary>Puts one row of each pane's own shape in, with content of a real length.</summary>
|
||||
/// <remarks>
|
||||
/// The local pane is cleared first for the reason <see cref="TheRowTemplateShowsSizeAndDateColumns"/>
|
||||
/// gives: it lists this fixture's real home directory, and a virtualizing ListBox realises only the rows
|
||||
/// that fit — a row appended after those would never be built, and the assertion would find nothing to
|
||||
/// have missed.
|
||||
/// </remarks>
|
||||
private void SeedBothListings()
|
||||
{
|
||||
transfers.IsConnected = true;
|
||||
transfers.RemotePath = "/root";
|
||||
|
||||
transfers.RemoteEntries.Add(new RemoteEntryRowViewModel(new SftpEntry(
|
||||
".bash_history",
|
||||
"/root/.bash_history",
|
||||
SftpEntryKind.File,
|
||||
2_150,
|
||||
new DateTimeOffset(2026, 8, 21, 11, 36, 0, TimeSpan.Zero),
|
||||
"-rw-r--r--")));
|
||||
|
||||
transfers.LocalEntries.Clear();
|
||||
transfers.LocalEntries.Add(new LocalEntryRowViewModel(new LocalEntry(
|
||||
".azure-devops",
|
||||
Path.Combine(Path.GetTempPath(), ".azure-devops"),
|
||||
IsDirectory: true,
|
||||
Length: 0,
|
||||
new DateTimeOffset(2026, 7, 4, 9, 0, 0, TimeSpan.Zero))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What a session-shell screen gets on a maximised window, which is where the columns were reported
|
||||
/// wrong.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A 1843-pixel window less the shell's own 26-pixel padding a side, its 1-pixel border a side, and
|
||||
/// QUICK ACCESS's 300 — the arithmetic <see cref="LayoutHarness.SessionScreenWidth"/> does, at a width
|
||||
/// that constant cannot express because it is pinned to the window's <em>minimum</em>. Both belong in
|
||||
/// this file: the minimum is what everything else here measures against, and this is the width at which
|
||||
/// the numeric columns reach their caps and the question "does the content fit" has a yes to give.
|
||||
/// Below it they share the width instead, which is what
|
||||
/// <see cref="TheNameColumnSurvivesTheNarrowestBudget"/> holds the other end of.
|
||||
/// </remarks>
|
||||
private const double MaximisedSessionScreenWidth = 1843 - (2 * 26) - (2 * 1) - 300;
|
||||
|
||||
/// <summary>What a rendered block would want if nothing bounded it.</summary>
|
||||
private static double Unconstrained(TextBlock block) =>
|
||||
Measured(block.Text ?? string.Empty, block.FontSize, block.FontFamily);
|
||||
|
||||
/// <inheritdoc cref="Unconstrained" />
|
||||
private static double Measured(string text, double fontSize, Avalonia.Media.FontFamily family)
|
||||
{
|
||||
var block = new TextBlock { Text = text, FontSize = fontSize, FontFamily = family };
|
||||
|
||||
block.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
|
||||
return block.DesiredSize.Width;
|
||||
}
|
||||
|
||||
private Task MeasureTransfersAsync(Action<IReadOnlyList<string>> assert) =>
|
||||
LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
|
||||
Reference in new issue
Block a user