Skip to content

Commit c2cfe52

Browse files
committed
fix(render): refresh baselines and restore datatree chrome
1 parent 80c5ba6 commit c2cfe52

25 files changed

Lines changed: 111 additions & 11 deletions

File tree

-20 Bytes
Loading
144 Bytes
Loading
234 Bytes
Loading
191 Bytes
Loading
143 Bytes
Loading
177 Bytes
Loading
Loading
6.96 KB
Loading

Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeRenderTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ namespace SIL.FieldWorks.Common.Framework.DetailControls
3737
public class DataTreeRenderTests : MemoryOnlyBackendProviderRestoredForEachTestTestBase
3838
{
3939
private const string DeterministicRenderFontFamily = "Segoe UI";
40+
private const string UpdateBaselinesEnvVar = "FW_UPDATE_RENDER_BASELINES";
4041
private const int MaxAllowedPixelDifferences = 4;
4142
private ILexEntry m_entry;
4243

@@ -397,6 +398,8 @@ private async Task VerifyDataTreeBitmap(Bitmap bitmap, string scenarioId)
397398
string receivedPath = Path.Combine(directory, $"{name}.received.png");
398399
string diffPath = Path.Combine(directory, $"{name}.diff.png");
399400

401+
RefreshVerifiedBaselineIfRequested(bitmap, verifiedPath);
402+
400403
if (File.Exists(diffPath))
401404
File.Delete(diffPath);
402405

@@ -430,6 +433,14 @@ private async Task VerifyDataTreeBitmap(Bitmap bitmap, string scenarioId)
430433
await Task.CompletedTask;
431434
}
432435

436+
private static void RefreshVerifiedBaselineIfRequested(Bitmap bitmap, string verifiedPath)
437+
{
438+
if (!string.Equals(Environment.GetEnvironmentVariable(UpdateBaselinesEnvVar), "1", StringComparison.Ordinal))
439+
return;
440+
441+
bitmap.Save(verifiedPath, ImageFormat.Png);
442+
}
443+
433444
private static void DeleteIfPresent(string path)
434445
{
435446
if (File.Exists(path))

Src/Common/RenderVerification/CompositeViewCapture.cs

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Drawing;
88
using System.Drawing.Imaging;
9+
using System.Reflection;
910
using System.Windows.Forms;
1011
using SIL.FieldWorks.Common.Framework.DetailControls;
1112
using SIL.FieldWorks.Common.RootSites;
@@ -19,7 +20,10 @@ namespace SIL.FieldWorks.Common.RenderVerification
1920
/// splitters, section headers) but produces black rectangles for Views engine content
2021
/// inside <see cref="ViewSlice"/> controls. The fix is a two-pass approach:
2122
/// <list type="number">
22-
/// <item>Capture the entire DataTree via <c>DrawToBitmap</c> (gets all WinForms chrome)</item>
23+
/// <item>Capture each slice locally via <c>DrawToBitmap</c> and composite it into the
24+
/// full bitmap (gets per-slice WinForms chrome without relying on giant control coordinates)</item>
25+
/// <item>Repaint DataTree-owned separator rules directly onto the bitmap, since those
26+
/// thin gray lines are drawn by the parent control rather than by each slice</item>
2327
/// <item>For each <see cref="ViewSlice"/>, render its <c>RootBox</c> via
2428
/// <c>VwDrawRootBuffered</c> into the correct region, overlaying the black rectangles</item>
2529
/// </list>
@@ -81,21 +85,22 @@ public static Bitmap CaptureDataTree(DataTree dataTree)
8185
dataTree.ClientSize = new Size(width, totalHeight);
8286
dataTree.PerformLayout();
8387

84-
// Pass 1: Capture WinForms chrome via DrawToBitmap.
85-
// Pre-fill with white so areas beyond slice content (and the
86-
// DataTree's grey SystemColors.Control background) render as
87-
// white, producing deterministic output regardless of system
88-
// theme or control background color.
88+
// Pass 1: Capture WinForms chrome slice-by-slice.
89+
// A single DrawToBitmap over a very tall DataTree can drop left-side
90+
// labels and separator lines for slices near the bottom of the image.
91+
// Compositing each slice locally avoids that large-coordinate WinForms
92+
// capture failure while preserving the existing root-box overlay pass.
8993
var bitmap = new Bitmap(width, totalHeight, PixelFormat.Format32bppArgb);
90-
using (var g = Graphics.FromImage(bitmap))
91-
{
92-
g.Clear(Color.White);
93-
}
94-
dataTree.DrawToBitmap(bitmap, new Rectangle(0, 0, width, totalHeight));
94+
CaptureSliceChrome(dataTree, bitmap);
9595

9696
// Pass 2: Overlay Views engine content for each ViewSlice
9797
OverlayViewSliceContent(dataTree, bitmap);
9898

99+
// Pass 3: Repaint parent-drawn separator rules.
100+
// These thin gray lines come from DataTree.OnPaint rather than from the
101+
// slice controls, so the slice-by-slice pass above will not capture them.
102+
PaintDataTreeSeparators(dataTree, bitmap, width, totalHeight);
103+
99104
return bitmap;
100105
}
101106
finally
@@ -193,6 +198,90 @@ private static void EnsureAllSlicesInitialized(DataTree dataTree)
193198
dataTree.PerformLayout();
194199
}
195200

201+
/// <summary>
202+
/// Captures WinForms chrome one slice at a time and composites the result into the
203+
/// final bitmap. This avoids whole-control <c>DrawToBitmap</c> failures when slice
204+
/// coordinates become very large in tall DataTree captures.
205+
/// </summary>
206+
private static void CaptureSliceChrome(DataTree dataTree, Bitmap bitmap)
207+
{
208+
using (var mainGraphics = Graphics.FromImage(bitmap))
209+
{
210+
mainGraphics.Clear(Color.White);
211+
212+
if (dataTree.Slices == null)
213+
return;
214+
215+
foreach (Slice slice in dataTree.Slices)
216+
{
217+
if (slice == null)
218+
continue;
219+
220+
try
221+
{
222+
CaptureSingleSliceChrome(slice, dataTree, mainGraphics);
223+
}
224+
catch (Exception ex)
225+
{
226+
Trace.TraceWarning(
227+
$"[CompositeViewCapture] Failed to capture slice chrome '{slice.Label}': {ex.Message}");
228+
}
229+
}
230+
}
231+
}
232+
233+
private static void CaptureSingleSliceChrome(Slice slice, DataTree dataTree, Graphics mainGraphics)
234+
{
235+
var sliceRect = GetControlRectRelativeTo(slice, dataTree);
236+
if (sliceRect.Width <= 0 || sliceRect.Height <= 0)
237+
return;
238+
239+
using (var sliceBitmap = new Bitmap(sliceRect.Width, sliceRect.Height, PixelFormat.Format32bppArgb))
240+
{
241+
using (var sliceGraphics = Graphics.FromImage(sliceBitmap))
242+
{
243+
sliceGraphics.Clear(Color.White);
244+
}
245+
246+
slice.DrawToBitmap(sliceBitmap, new Rectangle(0, 0, sliceRect.Width, sliceRect.Height));
247+
mainGraphics.DrawImageUnscaled(sliceBitmap, sliceRect.Location);
248+
}
249+
}
250+
251+
private static void PaintDataTreeSeparators(DataTree dataTree, Bitmap bitmap, int width, int totalHeight)
252+
{
253+
const BindingFlags privateInstance = BindingFlags.Instance | BindingFlags.NonPublic;
254+
var paintLinesMethod = typeof(DataTree).GetMethod("PaintLinesBetweenSlices", privateInstance);
255+
if (paintLinesMethod == null)
256+
{
257+
Trace.TraceWarning("[CompositeViewCapture] Could not locate DataTree.PaintLinesBetweenSlices.");
258+
return;
259+
}
260+
261+
using (var graphics = Graphics.FromImage(bitmap))
262+
{
263+
try
264+
{
265+
paintLinesMethod.Invoke(dataTree, new object[]
266+
{
267+
graphics,
268+
new Rectangle(0, 0, width, totalHeight),
269+
width,
270+
});
271+
}
272+
catch (TargetInvocationException ex)
273+
{
274+
Trace.TraceWarning(
275+
$"[CompositeViewCapture] Failed to paint DataTree separators: {ex.InnerException?.Message ?? ex.Message}");
276+
}
277+
catch (Exception ex)
278+
{
279+
Trace.TraceWarning(
280+
$"[CompositeViewCapture] Failed to paint DataTree separators: {ex.Message}");
281+
}
282+
}
283+
}
284+
196285
/// <summary>
197286
/// Iterates all ViewSlice descendants and renders their RootBox content
198287
/// via VwDrawRootBuffered into the correct region of the bitmap.

0 commit comments

Comments
 (0)