Skip to content

Commit bbbb82f

Browse files
marcosqlbiclaude
andauthored
Reflow drawn setting choices instead of clipping their labels (#79)
The drawn choices added in #73 — Pen button, Toolbar position, Toolbar layout — were clipped when the Preferences dialog was narrowed. Toolbar / Position is the one that showed it: five options in a row, and at the dialog's 560 px minimum each segment holds about 52 px inside its padding, against the ~44 px "Bottom" needs. A word too long for its line overflows and is clipped rather than wrapped, and `TextTrimming` does not help because it applies only when the height is constrained — so the label read as a different word, "Bottor right". ## The row reflows The choices now sit in a small panel that measures and arranges its own children, taking its column count from the width the row actually occupies. Five choices stay in one row when there is room and fall to 4+1 and then 3+2 as the dialog narrows, rather than shrinking past legibility. Two earlier attempts are worth recording, because both looked right and neither was. Recomputing on `SizeChanged` only fires when the width changes, so the count settled during the first layout pass stood for the life of the dialog. Moving it to `MeasureOverride` fixed the staleness but not the cause: the width a row is measured against arrives far narrower than the width it is finally given, so the count was still computed against a width the row never had. `ArrangeOverride` has the real width, and that is what it now reads. ## The samples scale Each sample is wrapped in a down-only `Viewbox`, so it draws at its designed size when there is room and shrinks with the column instead of overflowing and losing its edges. The segment template honours `HorizontalContentAlignment` so the label is given the width it is measured against, and the padding drops from `10,12` to `6,12`. Version 1.1.4. Release build clean with `TreatWarningsAsErrors`, smoke tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 32997a7 commit bbbb82f

4 files changed

Lines changed: 111 additions & 14 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
scripts/build-installer.ps1 both read it from here, so releasing is a reviewed change
66
to this line rather than an edit in a pipeline variable group.
77
-->
8-
<VersionPrefix>1.1.2</VersionPrefix>
8+
<VersionPrefix>1.1.4</VersionPrefix>
99
<LangVersion>latest</LangVersion>
1010
<Nullable>enable</Nullable>
1111
<ImplicitUsings>enable</ImplicitUsings>

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The delivery chain works end to end: a merge to `main` builds, signs, and publis
1616
pre-release to GitHub Releases, and one approval promotes that same build to a release.
1717
<https://whiteboard.sqlbi.com> reads its download links from the release manifest
1818
deployed beside it and needs no edit per release. The current product version is `VersionPrefix` in `Directory.Build.props`
19-
(1.1.2). Identity version for the Store package is `VersionPrefix.0` (`1.1.2.0`).
19+
(1.1.4). Identity version for the Store package is `VersionPrefix.0` (`1.1.4.0`).
2020

2121
Declaring that number is decision 20 in [docs/decisions.md](docs/decisions.md). What 1.0
2222
was waiting on shipped during 0.9.x: Preferences, `.wimport`, Explorer and VS Code

src/SQLBI.Whiteboard/PreferencesWindow.xaml.cs

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,7 @@ private FrameworkElement CreateSampleChoice(
229229
SettingDescriptor setting,
230230
Func<string, FrameworkElement?> sampleFor)
231231
{
232-
var host = new UniformGrid
233-
{
234-
Rows = 1,
235-
Columns = setting.Choices.Count,
236-
};
237-
232+
var host = new ReflowingSegments();
238233
var segments = new List<ToggleButton>();
239234
foreach (var choice in setting.Choices)
240235
{
@@ -243,17 +238,33 @@ private FrameworkElement CreateSampleChoice(
243238
continue;
244239
}
245240

246-
var content = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
241+
var content = new StackPanel { HorizontalAlignment = HorizontalAlignment.Stretch };
242+
243+
// The sample is drawn at a fixed size and then allowed to shrink
244+
// with the column. Left to its own width it overflowed a narrowed
245+
// dialog and was clipped, edges first, which is worse than small.
247246
sample.HorizontalAlignment = HorizontalAlignment.Center;
248-
content.Children.Add(sample);
247+
content.Children.Add(new Viewbox
248+
{
249+
Child = sample,
250+
Stretch = Stretch.Uniform,
251+
StretchDirection = StretchDirection.DownOnly,
252+
MaxWidth = sample.Width > 0 ? sample.Width : double.PositiveInfinity,
253+
HorizontalAlignment = HorizontalAlignment.Center,
254+
});
255+
256+
// Wrapping runs out at the longest word, and a clipped word reads as
257+
// a different one. Past that point the picture carries the meaning,
258+
// and the tooltip still spells it out.
249259
content.Children.Add(new TextBlock
250260
{
251261
Style = (Style)FindResource("SettingsValueLabel"),
252262
Text = choice.Title,
253263
Margin = new Thickness(0, 8, 0, 0),
254264
TextWrapping = TextWrapping.Wrap,
265+
TextTrimming = TextTrimming.CharacterEllipsis,
255266
TextAlignment = TextAlignment.Center,
256-
HorizontalAlignment = HorizontalAlignment.Center,
267+
HorizontalAlignment = HorizontalAlignment.Stretch,
257268
});
258269

259270
var segment = new ToggleButton
@@ -263,6 +274,7 @@ private FrameworkElement CreateSampleChoice(
263274
IsChecked = choice.Id == CurrentEnumId(setting),
264275
Tag = choice.Id,
265276
ToolTip = choice.Title,
277+
HorizontalContentAlignment = HorizontalAlignment.Stretch,
266278
};
267279
segment.Click += (_, _) =>
268280
{
@@ -280,6 +292,91 @@ private FrameworkElement CreateSampleChoice(
280292
return host;
281293
}
282294

295+
/// <summary>
296+
/// A row of drawn choices that takes a second row rather than squeezing its
297+
/// labels. Below <see cref="MinimumSegmentWidth"/> a segment cannot hold the
298+
/// longest word in a label - "Bottom" is 44px at the 12px label size, and
299+
/// the padding and border take the rest - and a word too long for its line
300+
/// overflows and is clipped rather than wrapped or trimmed, so it reads as a
301+
/// different word.
302+
/// </summary>
303+
private sealed class ReflowingSegments : Panel
304+
{
305+
private const double MinimumSegmentWidth = 82;
306+
307+
// The width a row is measured against and the width it is finally given
308+
// are not the same here: measurement arrives far narrower than the
309+
// arrangement, so a count settled during measure put five choices on
310+
// three columns in a row with room for all five. Arrange has the real
311+
// width, so that is what the count is taken from, and the children are
312+
// measured again if it disagrees with what measure assumed.
313+
protected override Size MeasureOverride(Size availableSize)
314+
{
315+
var count = InternalChildren.Count;
316+
if (count == 0)
317+
{
318+
return default;
319+
}
320+
321+
var columns = ColumnsFor(availableSize.Width, count);
322+
var cell = MeasureCells(columns, availableSize.Width, count);
323+
return new Size(
324+
double.IsInfinity(availableSize.Width)
325+
? cell.Width * columns
326+
: availableSize.Width,
327+
cell.Height * RowsFor(count, columns));
328+
}
329+
330+
protected override Size ArrangeOverride(Size finalSize)
331+
{
332+
var count = InternalChildren.Count;
333+
if (count == 0)
334+
{
335+
return finalSize;
336+
}
337+
338+
var columns = ColumnsFor(finalSize.Width, count);
339+
MeasureCells(columns, finalSize.Width, count);
340+
341+
var cellWidth = finalSize.Width / columns;
342+
var cellHeight = finalSize.Height / RowsFor(count, columns);
343+
for (var index = 0; index < count; index++)
344+
{
345+
InternalChildren[index].Arrange(new Rect(
346+
index % columns * cellWidth,
347+
index / columns * cellHeight,
348+
cellWidth,
349+
cellHeight));
350+
}
351+
352+
return finalSize;
353+
}
354+
355+
private static int ColumnsFor(double width, int count) => Math.Clamp(
356+
double.IsInfinity(width) ? count : (int)(width / MinimumSegmentWidth),
357+
1,
358+
count);
359+
360+
private static int RowsFor(int count, int columns) =>
361+
((count - 1) / columns) + 1;
362+
363+
private Size MeasureCells(int columns, double width, int count)
364+
{
365+
var cellWidth = double.IsInfinity(width)
366+
? MinimumSegmentWidth
367+
: width / columns;
368+
var tallest = 0d;
369+
for (var index = 0; index < count; index++)
370+
{
371+
var child = InternalChildren[index];
372+
child.Measure(new Size(cellWidth, double.PositiveInfinity));
373+
tallest = Math.Max(tallest, child.DesiredSize.Height);
374+
}
375+
376+
return new Size(cellWidth, tallest);
377+
}
378+
}
379+
283380
private static readonly Brush SampleInkBrush = Frozen(0xFF374151);
284381
private static readonly Brush SampleGhostBrush = Frozen(0x59374151);
285382
private static readonly Brush SampleBoardBrush = Frozen(0xFFFFFFFF);

src/SQLBI.Whiteboard/Themes/Settings.xaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,9 @@
300300
<Style x:Key="SettingsSampleSegment"
301301
TargetType="ToggleButton">
302302
<Setter Property="Margin"
303-
Value="4,0" />
303+
Value="3,0" />
304304
<Setter Property="Padding"
305-
Value="10,12" />
305+
Value="6,12" />
306306
<Setter Property="Focusable"
307307
Value="False" />
308308
<Setter Property="Cursor"
@@ -319,7 +319,7 @@
319319
BorderBrush="#FFE4E6EA"
320320
Background="{DynamicResource SettingsEditorBrush}"
321321
SnapsToDevicePixels="True">
322-
<ContentPresenter HorizontalAlignment="Center" />
322+
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" />
323323
</Border>
324324
<ControlTemplate.Triggers>
325325
<Trigger Property="IsMouseOver"

0 commit comments

Comments
 (0)