Cut CPU use and move heavy work off the UI thread
Redraws: - Merge bursts of session and bandmap events into one redraw (Coalesced). Closed Bandmap and Available windows now unsubscribe. - Grayline and Voice Keyer no longer subscribe to the session twice. - Available summary, Score table and Log rows redraw only when their contents change. Telnet lines are added in batches. - Bandmap view reuses its layout, shaped text and brushes. Grayline draws the coastlines as one cached shape. Threads: - Available, Bandmap colours, Check and Statistics do their judging and counting on the thread pool and draw the result on the UI thread. - ContestLog takes a lock in Judge and in each method that changes the log, so a background Judge cannot read the indexes mid-edit. Refresh rate: - Statistics refreshes every 30 seconds and when the QSO count changes. - Grayline refreshes at most every 15 seconds on session events. Publish with ReadyToRun to cut startup JIT time. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -99,14 +99,35 @@ public sealed class BandmapView : Control
|
||||
DrawSegments(context);
|
||||
DrawScale(context, ink);
|
||||
DrawVfos(context);
|
||||
placed = [.. BandmapLayout.Place(Spots, scale, LabelHeight)
|
||||
.Select(l => l with { MarkY = l.MarkY + Inset, LabelY = l.LabelY + Inset })];
|
||||
PlaceLabels();
|
||||
foreach (BandmapLabel label in placed)
|
||||
{
|
||||
DrawLabel(context, label, ink);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<Spot> placedSpots = [];
|
||||
private BandmapScale? placedScale;
|
||||
private double placedHeight;
|
||||
|
||||
/// Lays the callsigns out again only when the spots, the scale or the text
|
||||
/// size are not what the last layout used. Redraws for any other reason, a
|
||||
/// moving receiver bar for one, reuse it.
|
||||
private void PlaceLabels()
|
||||
{
|
||||
if (placedScale == scale
|
||||
&& placedHeight == LabelHeight
|
||||
&& (ReferenceEquals(placedSpots, Spots) || placedSpots.SequenceEqual(Spots)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
placedSpots = Spots;
|
||||
placedScale = scale;
|
||||
placedHeight = LabelHeight;
|
||||
placed = [.. BandmapLayout.Place(Spots, scale, LabelHeight)
|
||||
.Select(l => l with { MarkY = l.MarkY + Inset, LabelY = l.LabelY + Inset })];
|
||||
}
|
||||
|
||||
/// N1MM paints the CW, digital and phone parts of the band behind the
|
||||
/// scale, the digital block over the other two.
|
||||
/// Anything past a band edge is painted as out of band, which is the only
|
||||
@@ -209,12 +230,20 @@ public sealed class BandmapView : Control
|
||||
protected override void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
base.OnPointerMoved(e);
|
||||
Spot? under = LabelAt(e.GetPosition(this))?.Spot;
|
||||
if (Equals(under, tipFor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
tipFor = under;
|
||||
ToolTip.SetShowDelay(this, 0);
|
||||
ToolTip.SetTip(
|
||||
this,
|
||||
LabelAt(e.GetPosition(this)) is { } label ? SpotDescription.Of(label.Spot) : null);
|
||||
ToolTip.SetTip(this, under is null ? null : SpotDescription.Of(under));
|
||||
}
|
||||
|
||||
/// The spot the tool tip describes, so moving the pointer along a callsign
|
||||
/// does not build the text again.
|
||||
private Spot? tipFor;
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
@@ -227,23 +256,56 @@ public sealed class BandmapView : Control
|
||||
FrequencyPicked?.Invoke(this, scale.At(at.Y - Inset));
|
||||
}
|
||||
|
||||
private FormattedText Text(string text, IBrush brush, double size, bool bold = false, bool italic = false) =>
|
||||
new(
|
||||
/// Shaping text is the dearest part of a redraw, and the same few hundred
|
||||
/// callsigns and frequency numbers are drawn every time, so each is kept.
|
||||
private readonly Dictionary<(string Text, IBrush Brush, double Size, bool Bold, bool Italic), FormattedText> texts = [];
|
||||
|
||||
private FormattedText Text(string text, IBrush brush, double size, bool bold = false, bool italic = false)
|
||||
{
|
||||
if (texts.TryGetValue((text, brush, size, bold, italic), out FormattedText? kept))
|
||||
{
|
||||
return kept;
|
||||
}
|
||||
if (texts.Count > 2000)
|
||||
{
|
||||
texts.Clear();
|
||||
}
|
||||
FormattedText made = new(
|
||||
text,
|
||||
CultureInfo.InvariantCulture,
|
||||
FlowDirection.LeftToRight,
|
||||
bold ? boldTypeface : italic ? italicTypeface : typeface,
|
||||
size,
|
||||
brush);
|
||||
texts[(text, brush, size, bold, italic)] = made;
|
||||
return made;
|
||||
}
|
||||
|
||||
/// The form's text colour, which the theme sets in both light and dark.
|
||||
private IBrush Foreground() =>
|
||||
this.FindResource("FormForeground") as IBrush ?? Brushes.Gray;
|
||||
|
||||
private static IBrush Opaque(IBrush ink, double opacity) =>
|
||||
ink is ISolidColorBrush solid
|
||||
? new SolidColorBrush(solid.Color, opacity)
|
||||
: ink;
|
||||
private readonly Dictionary<(IBrush Ink, double Opacity), IBrush> faded = [];
|
||||
|
||||
/// Kept, so the same brush comes back each time and the text drawn with it
|
||||
/// is found in `texts`.
|
||||
private IBrush Opaque(IBrush ink, double opacity)
|
||||
{
|
||||
if (ink is not ISolidColorBrush solid)
|
||||
{
|
||||
return ink;
|
||||
}
|
||||
if (!faded.TryGetValue((ink, opacity), out IBrush? brush))
|
||||
{
|
||||
if (faded.Count > 50)
|
||||
{
|
||||
faded.Clear();
|
||||
}
|
||||
brush = new SolidColorBrush(solid.Color, opacity);
|
||||
faded[(ink, opacity)] = brush;
|
||||
}
|
||||
return brush;
|
||||
}
|
||||
|
||||
private static IBrush BrushFor(VfoRole role) => role switch
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user