Skip to content

Commit 846efe2

Browse files
sjvrensburgclaude
andcommitted
refactor(freeze-panes): cleanup pass + font-scaled flyout text + tall skinny layout
/simplify pass on the freeze-panes flyout: dropped a redundant _freezeFlyoutOpen shadow field in favour of FlyoutBase.IsOpen, extracted a shared ClearFreezeGuide() helper replacing four inlined copies of the same guide/cursor reset in ViewportPanel, and bounded the placement-shading rect to the surface's actual size instead of an oversized fixed-extent fill repainted every frame. The flyout's caption and mode-button labels also now track UiFontScale (they previously fell back to fixed 11px/10px regardless of the flyout's scaled root FontSize), and the three mode buttons switched from a 3-column grid to a vertical list with horizontal icon+label rows so they keep fitting at high UI scale instead of getting squeezed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 888aca0 commit 846efe2

6 files changed

Lines changed: 86 additions & 62 deletions

File tree

src/RailReader2/Views/DocumentView.axaml.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,8 @@ private FreezePaneRenderState BuildFreezePaneState(TabViewModel? tab)
631631
return new FreezePaneRenderState(
632632
corner, top, left, cornerDst, topDst, leftDst,
633633
vm.Controller.ActiveColourEffect, vm.Controller.ActiveColourIntensity, vm.ColourEffects,
634-
showGuide, guideH, guideV, guideX, guideY);
634+
showGuide, guideH, guideV, guideX, guideY,
635+
(float)Viewport.Bounds.Width, (float)Viewport.Bounds.Height);
635636

636637
static SKRect Dst(BBox box, float zoom, float x, float y)
637638
=> SKRect.Create(x, y, box.W * zoom, box.H * zoom);

src/RailReader2/Views/FreezePaneLayer.cs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ internal sealed record FreezePaneRenderState(
1818
SKRect CornerDst, SKRect TopDst, SKRect LeftDst,
1919
ColourEffect Effect, float EffectIntensity, ColourEffectShaders? Effects,
2020
// Armed freeze-mode guide line(s) at the pointer (screen-space): a horizontal line (rows → freeze
21-
// above), a vertical line (columns → freeze left), or both. Drawn full-length as accent guides so
22-
// the user aims before clicking; a translucent tint covers the region that would be frozen (above
23-
// the horizontal guide / left of the vertical guide) — the canvas is already clipped to this
24-
// surface's bounds, so a full-length fill is enough without needing the surface size.
25-
bool ShowGuide = false, bool GuideH = false, bool GuideV = false, float GuideX = 0, float GuideY = 0);
21+
// above), a vertical line (columns → freeze left), or both, plus a translucent tint over the
22+
// region that would be frozen (above the horizontal guide / left of the vertical guide).
23+
// ViewW/ViewH (the surface size) bound the tint fill so it doesn't paint far past what's visible.
24+
bool ShowGuide = false, bool GuideH = false, bool GuideV = false, float GuideX = 0, float GuideY = 0,
25+
float ViewW = 0, float ViewH = 0);
2626

2727
/// <summary>Hosts a CompositionCustomVisual that draws Excel-style frozen table panes (the rows above
2828
/// and columns left of the frozen cell) pinned over the live page while rail-reading a table.</summary>
@@ -103,29 +103,37 @@ public override void OnRender(ImmediateDrawingContext context)
103103
s_paint.ColorFilter = null;
104104

105105
// Armed freeze-mode guide: full-length line(s) at the pointer, so the user aims the page-wide
106-
// split before clicking. Drawn last, over everything, in an accent colour. A translucent tint
107-
// is painted first over the region that WOULD be frozen (above/left of the guide) so the user
108-
// sees exactly what a click commits, not just a bare line.
106+
// split before clicking, each paired with a translucent tint over the region that WOULD be
107+
// frozen (above/left of that guide) so the user sees exactly what a click commits, not just a
108+
// bare line. Drawn last, over everything, in an accent colour. The tint is bounded to the
109+
// surface's actual size (not an oversized fill) since it repaints every frame while armed.
109110
if (state.ShowGuide)
110111
{
111-
const float far = 100000f;
112+
const float far = 100000f; // guide lines still span off-screen; only the tint fill is bounded
113+
float viewW = state.ViewW > 0 ? state.ViewW : far;
114+
float viewH = state.ViewH > 0 ? state.ViewH : far;
112115
var shade = s_shadePaint ??= new SKPaint
113116
{
114117
Color = new SKColor(0x29, 0x9D, 0xF5, 0x28), // accent blue, ~15% alpha
115118
IsStroke = false,
116119
};
117-
if (state.GuideH) canvas.DrawRect(SKRect.Create(-far, -far, 2 * far, far + state.GuideY), shade);
118-
if (state.GuideV) canvas.DrawRect(SKRect.Create(-far, -far, far + state.GuideX, 2 * far), shade);
119-
120120
var line = s_guidePaint ??= new SKPaint
121121
{
122122
Color = new SKColor(0x29, 0x9D, 0xF5), // accent blue
123123
IsStroke = true,
124124
StrokeWidth = 2f,
125125
IsAntialias = true,
126126
};
127-
if (state.GuideH) canvas.DrawLine(-far, state.GuideY, far, state.GuideY, line); // freeze above
128-
if (state.GuideV) canvas.DrawLine(state.GuideX, -far, state.GuideX, far, line); // freeze left
127+
if (state.GuideH) // freeze above
128+
{
129+
canvas.DrawRect(SKRect.Create(0, 0, viewW, state.GuideY), shade);
130+
canvas.DrawLine(-far, state.GuideY, far, state.GuideY, line);
131+
}
132+
if (state.GuideV) // freeze left
133+
{
134+
canvas.DrawRect(SKRect.Create(0, 0, state.GuideX, viewH), shade);
135+
canvas.DrawLine(state.GuideX, -far, state.GuideX, far, line);
136+
}
129137
}
130138
}
131139
}

src/RailReader2/Views/FreezePanesView.axaml

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,35 @@
77
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
88
<StackPanel Margin="8,6" Spacing="6" MinWidth="200">
99
<TextBlock Text="Freeze panes" FontWeight="SemiBold" />
10-
<TextBlock Text="Pin rows / columns in place while the rest of the page scrolls — works on any page."
11-
TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
12-
<UniformGrid Columns="3">
13-
<ToggleButton Name="FreezeRowsButton" Margin="0,0,2,0"
14-
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
10+
<TextBlock Name="CaptionText"
11+
Text="Pin rows / columns in place while the rest of the page scrolls — works on any page."
12+
TextWrapping="Wrap" Opacity="0.7" />
13+
<StackPanel Spacing="4">
14+
<ToggleButton Name="FreezeRowsButton"
15+
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
1516
ToolTip.Tip="Then click in the page to freeze everything above a horizontal line.">
16-
<StackPanel Spacing="2" HorizontalAlignment="Center">
17-
<c:Icon Data="{StaticResource IconFreezeRows}" />
18-
<TextBlock Text="Rows" FontSize="10" HorizontalAlignment="Center" />
17+
<StackPanel Orientation="Horizontal" Spacing="8">
18+
<c:Icon Data="{StaticResource IconFreezeRows}" VerticalAlignment="Center" />
19+
<TextBlock Name="RowsLabel" Text="Rows" VerticalAlignment="Center" />
1920
</StackPanel>
2021
</ToggleButton>
21-
<ToggleButton Name="FreezeColumnsButton" Margin="2,0"
22-
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
22+
<ToggleButton Name="FreezeColumnsButton"
23+
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
2324
ToolTip.Tip="Then click in the page to freeze everything left of a vertical line.">
24-
<StackPanel Spacing="2" HorizontalAlignment="Center">
25-
<c:Icon Data="{StaticResource IconFreezeColumns}" />
26-
<TextBlock Text="Columns" FontSize="10" HorizontalAlignment="Center" />
25+
<StackPanel Orientation="Horizontal" Spacing="8">
26+
<c:Icon Data="{StaticResource IconFreezeColumns}" VerticalAlignment="Center" />
27+
<TextBlock Name="ColumnsLabel" Text="Columns" VerticalAlignment="Center" />
2728
</StackPanel>
2829
</ToggleButton>
29-
<ToggleButton Name="FreezeBothButton" Margin="2,0,0,0"
30-
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
30+
<ToggleButton Name="FreezeBothButton"
31+
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
3132
ToolTip.Tip="Then click to freeze above and left of a crossing point. Shortcut: Z">
32-
<StackPanel Spacing="2" HorizontalAlignment="Center">
33-
<c:Icon Data="{StaticResource IconFreezeBoth}" />
34-
<TextBlock Text="Both" FontSize="10" HorizontalAlignment="Center" />
33+
<StackPanel Orientation="Horizontal" Spacing="8">
34+
<c:Icon Data="{StaticResource IconFreezeBoth}" VerticalAlignment="Center" />
35+
<TextBlock Name="BothLabel" Text="Both" VerticalAlignment="Center" />
3536
</StackPanel>
3637
</ToggleButton>
37-
</UniformGrid>
38+
</StackPanel>
3839
<Button Name="UnfreezeButton" HorizontalAlignment="Stretch" Content="Unfreeze"
3940
ToolTip.Tip="Clear this view's freeze." />
4041
</StackPanel>

src/RailReader2/Views/FreezePanesView.axaml.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel;
2+
using Avalonia;
23
using Avalonia.Controls;
34
using Avalonia.Interactivity;
45
using RailReader2.ViewModels;
@@ -29,6 +30,31 @@ public FreezePanesView()
2930
// Hosted in the toolbar's Freeze flyout, so this control is loaded/unloaded on every open/close.
3031
// (Re)wire to the VM on both DataContext change and load so re-opening the flyout stays live.
3132
DataContextChanged += (_, _) => Rewire();
33+
34+
RescaleText(); // seed from the default FontSize before ToolBarView pushes the scaled one
35+
}
36+
37+
// ToolBarView sets our FontSize to the scaled UI font size (this flyout is a popup in its own
38+
// visual root, so it doesn't inherit Window.FontSize / UiFontScale on its own — see
39+
// ToolBarView.UpdateFreezeFontScale). The caption and mode labels are deliberately smaller than the
40+
// header, so they can't just inherit FontSize outright — instead they're kept at the same ratio to
41+
// it as the original fixed-px design (11px / 10px against a 14px base), so they scale in step.
42+
private const double CaptionSizeRatio = 11.0 / 14.0;
43+
private const double LabelSizeRatio = 10.0 / 14.0;
44+
45+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
46+
{
47+
base.OnPropertyChanged(change);
48+
if (change.Property == FontSizeProperty)
49+
RescaleText();
50+
}
51+
52+
private void RescaleText()
53+
{
54+
CaptionText.FontSize = FontSize * CaptionSizeRatio;
55+
RowsLabel.FontSize = FontSize * LabelSizeRatio;
56+
ColumnsLabel.FontSize = FontSize * LabelSizeRatio;
57+
BothLabel.FontSize = FontSize * LabelSizeRatio;
3258
}
3359

3460
protected override void OnLoaded(RoutedEventArgs e)

src/RailReader2/Views/ToolBarView.axaml.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public partial class ToolBarView : UserControl
1919
// to the same VM. Freeze is page-wide and table-independent, so the button is always available.
2020
private readonly Flyout _freezeFlyout = new();
2121
private readonly FreezePanesView _freezeView = new();
22-
private bool _freezeFlyoutOpen;
2322

2423
public MainWindowViewModel? ViewModel
2524
{
@@ -68,11 +67,9 @@ public ToolBarView()
6867
// with the toolbar's bottom-left edge, regardless of where the Freeze button sits in the row.
6968
// Button.Flyout would anchor to the button itself instead, so this is wired manually.
7069
_freezeFlyout.Placement = PlacementMode.BottomEdgeAlignedLeft;
71-
_freezeFlyout.Opened += (_, _) => _freezeFlyoutOpen = true;
72-
_freezeFlyout.Closed += (_, _) => _freezeFlyoutOpen = false;
7370
FreezeButton.Click += (_, _) =>
7471
{
75-
if (_freezeFlyoutOpen) _freezeFlyout.Hide();
72+
if (_freezeFlyout.IsOpen) _freezeFlyout.Hide();
7673
else _freezeFlyout.ShowAt(RootBorder);
7774
};
7875
}

src/RailReader2/Views/ViewportPanel.cs

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,7 @@ protected override void OnPointerPressed(PointerPressedEventArgs e)
113113
if (vm.FreezeArmMode != FreezeMode.None)
114114
{
115115
vm.FreezeArmMode = FreezeMode.None;
116-
if (_freezeGuidePushed)
117-
{
118-
OwnerView?.SetFreezeGuide(FreezeMode.None, 0, 0);
119-
Cursor = Cursor.Default;
120-
_freezeGuidePushed = false;
121-
}
116+
ClearFreezeGuide();
122117
}
123118
else if (vm.ActiveTool == AnnotationTool.TextSelect && vm.SelectedText is not null)
124119
{
@@ -199,12 +194,7 @@ protected override void OnPointerMoved(PointerEventArgs e)
199194
_freezeGuidePushed = true;
200195
return;
201196
}
202-
if (_freezeGuidePushed)
203-
{
204-
OwnerView?.SetFreezeGuide(FreezeMode.None, 0, 0);
205-
Cursor = Cursor.Default; // the link/annotation cursor logic re-applies on the next move
206-
_freezeGuidePushed = false;
207-
}
197+
ClearFreezeGuide(); // the link/annotation cursor logic re-applies on the next move, below
208198

209199
// Not dragging — update cursor for link hover (only in browse mode)
210200
if (!ViewModel.IsAnnotating)
@@ -266,19 +256,25 @@ protected override void OnPointerExited(PointerEventArgs e)
266256
// Pointer left this pane: drop its armed guide line so it doesn't stay painted here while the
267257
// pointer (and a fresh guide) moves onto a different split pane. The move handler re-pushes it on
268258
// re-entry. (OnPointerMoved only clears the guide once disarmed, never on a plain cross-pane exit.)
269-
if (_freezeGuidePushed)
270-
{
271-
OwnerView?.SetFreezeGuide(FreezeMode.None, 0, 0);
272-
Cursor = Cursor.Default;
273-
_freezeGuidePushed = false;
274-
}
259+
ClearFreezeGuide();
275260
}
276261

277262
private bool _showingLinkCursor;
278263
// True while this pane is pushing a freeze-mode guide line, so it can be cleared once disarmed.
279264
private bool _freezeGuidePushed;
280265
private static readonly Cursor s_crossCursor = new(StandardCursorType.Cross);
281266

267+
/// <summary>Drop this pane's armed freeze-mode guide line and restore the normal cursor. No-op
268+
/// unless a guide is currently pushed. Shared by every place a freeze placement gets cancelled or
269+
/// committed: pointer-move disarm, pointer-exit, right-click cancel, and commit-on-click.</summary>
270+
private void ClearFreezeGuide()
271+
{
272+
if (!_freezeGuidePushed) return;
273+
OwnerView?.SetFreezeGuide(FreezeMode.None, 0, 0);
274+
Cursor = Cursor.Default;
275+
_freezeGuidePushed = false;
276+
}
277+
282278
private void UpdateLinkCursor(bool overLink)
283279
{
284280
if (overLink == _showingLinkCursor) return;
@@ -319,12 +315,7 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e)
319315
{
320316
var (pageX, pageY) = ScreenToPage(pos);
321317
ViewModel.PlaceFreeze(OwnerView?.SurfaceViewport, pageX, pageY);
322-
OwnerView?.SetFreezeGuide(FreezeMode.None, 0, 0); // commit clears the guide
323-
_freezeGuidePushed = false;
324-
// Restore the normal cursor here: clearing _freezeGuidePushed disables OnPointerMoved's
325-
// disarm reset, so without this the crosshair would stick. Link/annotation cursor logic
326-
// re-applies on the next move.
327-
Cursor = Cursor.Default;
318+
ClearFreezeGuide(); // commit clears the guide; link/annotation cursor re-applies on the next move
328319
}
329320
// "Start rail here": when armed, a single click force-activates rail at the click point
330321
// (any zoom). Checked before markers/block-framing so it wins regardless of what's under it.

0 commit comments

Comments
 (0)