1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   TrackArtist.cpp
6 
7   Dominic Mazzoni
8 
9 
10 *******************************************************************//*!
11 
12 \class TrackArtist
13 \brief   This class handles the actual rendering of WaveTracks (both
14   waveforms and spectra), NoteTracks, LabelTracks and TimeTracks.
15 
16   It's actually a little harder than it looks, because for
17   waveforms at least it needs to cache the samples that are
18   currently on-screen.
19 
20 <b>How Audacity Redisplay Works \n
21  Roger Dannenberg</b> \n
22 Oct 2010 \n
23 
24 In my opinion, the bitmap should contain only the waveform, note, and
25 label images along with gray selection highlights. The track info
26 (sliders, buttons, title, etc.), track selection highlight, cursor, and
27 indicator should be drawn in the normal way, and clipping regions should
28 be used to avoid excessive copying of bitmaps (say, when sliders move),
29 or excessive redrawing of track info widgets (say, when scrolling occurs).
30 This is a fairly tricky code change since it requires careful specification
31 of what and where redraw should take place when any state changes. One
32 surprising finding is that NoteTrack display is slow compared to WaveTrack
33 display. Each note takes some time to gather attributes and select colors,
34 and while audio draws two amplitudes per horizontal pixels, large MIDI
35 scores can have more notes than horizontal pixels. This can make slider
36 changes very sluggish, but this can also be a problem with many
37 audio tracks.
38 
39 *//*******************************************************************/
40 
41 
42 #include "TrackArtist.h"
43 
44 
45 
46 #include "AColor.h"
47 #include "AllThemeResources.h"
48 #include "prefs/GUIPrefs.h"
49 #include "Theme.h"
50 #include "Track.h"
51 #include "TrackPanelDrawingContext.h"
52 #include "ViewInfo.h"
53 
54 #include "Decibels.h"
55 #include "prefs/TracksPrefs.h"
56 
57 #include <wx/app.h>
58 #include <wx/dc.h>
59 
60 //Thickness of the clip frame outline, shown when clip is dragged
61 static constexpr int ClipSelectionStrokeSize{ 1 };//px
62 
TrackArtist(TrackPanel * parent_)63 TrackArtist::TrackArtist( TrackPanel *parent_ )
64    : parent( parent_ )
65 {
66    mdBrange = DecibelScaleCutoff.GetDefault();
67    mShowClipping = false;
68    mSampleDisplay = 1;// Stem plots by default.
69 
70    SetColours(0);
71 
72    UpdatePrefs();
73 }
74 
~TrackArtist()75 TrackArtist::~TrackArtist()
76 {
77 }
78 
Get(TrackPanelDrawingContext & context)79 TrackArtist * TrackArtist::Get( TrackPanelDrawingContext &context )
80 {
81    return static_cast< TrackArtist* >( context.pUserData );
82 }
83 
SetColours(int iColorIndex)84 void TrackArtist::SetColours( int iColorIndex)
85 {
86    theTheme.SetBrushColour( blankBrush,      clrBlank );
87    theTheme.SetBrushColour( unselectedBrush, clrUnselected);
88    theTheme.SetBrushColour( selectedBrush,   clrSelected);
89    theTheme.SetBrushColour( sampleBrush,     clrSample);
90    theTheme.SetBrushColour( selsampleBrush,  clrSelSample);
91    theTheme.SetBrushColour( dragsampleBrush, clrDragSample);
92    theTheme.SetBrushColour( blankSelectedBrush, clrBlankSelected);
93 
94    theTheme.SetPenColour(   blankPen,        clrBlank);
95    theTheme.SetPenColour(   unselectedPen,   clrUnselected);
96    theTheme.SetPenColour(   selectedPen,     clrSelected);
97    theTheme.SetPenColour(   muteSamplePen,   clrMuteSample);
98    theTheme.SetPenColour(   odProgressDonePen, clrProgressDone);
99    theTheme.SetPenColour(   odProgressNotYetPen, clrProgressNotYet);
100    theTheme.SetPenColour(   shadowPen,       clrShadow);
101    theTheme.SetPenColour(   clippedPen,      clrClipped);
102    theTheme.SetPenColour(   muteClippedPen,  clrMuteClipped);
103    theTheme.SetPenColour(   blankSelectedPen,clrBlankSelected);
104 
105    theTheme.SetPenColour(   selsamplePen,    clrSelSample);
106    theTheme.SetPenColour(   muteRmsPen,      clrMuteRms);
107 
108    switch( iColorIndex %4 )
109    {
110       default:
111       case 0:
112          theTheme.SetPenColour(   samplePen,       clrSample);
113          theTheme.SetPenColour(   rmsPen,          clrRms);
114          break;
115       case 1: // RED
116          samplePen.SetColour( wxColor( 160,10,10 ) );
117          rmsPen.SetColour( wxColor( 230,80,80 ) );
118          break;
119       case 2: // GREEN
120          samplePen.SetColour( wxColor( 35,110,35 ) );
121          rmsPen.SetColour( wxColor( 75,200,75 ) );
122          break;
123       case 3: //BLACK
124          samplePen.SetColour( wxColor( 0,0,0 ) );
125          rmsPen.SetColour( wxColor( 100,100,100 ) );
126          break;
127 
128    }
129 }
130 
131 /// Takes a value between min and max and returns a value between
132 /// height and 0
133 /// \todo  Should this function move int GuiWaveTrack where it can
134 /// then use the zoomMin, zoomMax and height values without having
135 /// to have them passed in to it??
GetWaveYPos(float value,float min,float max,int height,bool dB,bool outer,float dBr,bool clip)136 int GetWaveYPos(float value, float min, float max,
137                 int height, bool dB, bool outer,
138                 float dBr, bool clip)
139 {
140    if (dB) {
141       if (height == 0) {
142          return 0;
143       }
144 
145       float sign = (value >= 0 ? 1 : -1);
146 
147       if (value != 0.) {
148          float db = LINEAR_TO_DB(fabs(value));
149          value = (db + dBr) / dBr;
150          if (!outer) {
151             value -= 0.5;
152          }
153          if (value < 0.0) {
154             value = 0.0;
155          }
156          value *= sign;
157       }
158    }
159    else {
160       if (!outer) {
161          if (value >= 0.0) {
162             value -= 0.5;
163          }
164          else {
165             value += 0.5;
166          }
167       }
168    }
169 
170    if (clip) {
171       if (value < min) {
172          value = min;
173       }
174       if (value > max) {
175          value = max;
176       }
177    }
178 
179    value = (max - value) / (max - min);
180    return (int) (value * (height - 1) + 0.5);
181 }
182 
FromDB(float value,double dBRange)183 float FromDB(float value, double dBRange)
184 {
185    if (value == 0)
186       return 0;
187 
188    double sign = (value >= 0 ? 1 : -1);
189    return DB_TO_LINEAR((fabs(value) * dBRange) - dBRange) * sign;
190 }
191 
ValueOfPixel(int yy,int height,bool offset,bool dB,double dBRange,float zoomMin,float zoomMax)192 float ValueOfPixel(int yy, int height, bool offset,
193    bool dB, double dBRange, float zoomMin, float zoomMax)
194 {
195    wxASSERT(height > 0);
196    // Map 0 to max and height - 1 (not height) to min
197    float v =
198       height == 1 ? (zoomMin + zoomMax) / 2 :
199       zoomMax - (yy / (float)(height - 1)) * (zoomMax - zoomMin);
200    if (offset) {
201       if (v > 0.0)
202          v += .5;
203       else
204          v -= .5;
205    }
206 
207    if (dB)
208       v = FromDB(v, dBRange);
209 
210    return v;
211 }
212 
DrawNegativeOffsetTrackArrows(TrackPanelDrawingContext & context,const wxRect & rect)213 void TrackArt::DrawNegativeOffsetTrackArrows(
214    TrackPanelDrawingContext &context, const wxRect &rect )
215 {
216    auto &dc = context.dc;
217 
218    // Draws two black arrows on the left side of the track to
219    // indicate the user that the track has been time-shifted
220    // to the left beyond t=0.0.
221 
222    dc.SetPen(*wxBLACK_PEN);
223    AColor::Line(dc,
224                 rect.x + 2, rect.y + 6,
225                 rect.x + 8, rect.y + 6);
226    AColor::Line(dc,
227                 rect.x + 2, rect.y + 6,
228                 rect.x + 6, rect.y + 2);
229    AColor::Line(dc,
230                 rect.x + 2, rect.y + 6,
231                 rect.x + 6, rect.y + 10);
232    AColor::Line(dc,
233                 rect.x + 2, rect.y + rect.height - 8,
234                 rect.x + 8, rect.y + rect.height - 8);
235    AColor::Line(dc,
236                 rect.x + 2, rect.y + rect.height - 8,
237                 rect.x + 6, rect.y + rect.height - 4);
238    AColor::Line(dc,
239                 rect.x + 2, rect.y + rect.height - 8,
240                 rect.x + 6, rect.y + rect.height - 12);
241 }
242 
TruncateText(wxDC & dc,const wxString & text,const int maxWidth)243 wxString TrackArt::TruncateText(wxDC& dc, const wxString& text, const int maxWidth)
244 {
245    static const wxString ellipsis = "\u2026";
246 
247    if (dc.GetTextExtent(text).GetWidth() <= maxWidth)
248        return text;
249 
250    auto left = 0;
251    //no need to check text + '...'
252    auto right = static_cast<int>(text.Length() - 2);
253 
254    while (left <= right)
255    {
256       auto middle = (left + right) / 2;
257       auto str = text.SubString(0, middle).Trim() + ellipsis;
258       auto strWidth = dc.GetTextExtent(str).GetWidth();
259       if (strWidth < maxWidth)
260          //if left == right (== middle), then exit loop
261          //with right equals to the last knwon index for which
262          //strWidth < maxWidth
263          left = middle + 1;
264       else if (strWidth > maxWidth)
265          //if right == left (== middle), then exit loop with
266          //right equals to (left - 1), which is the last known
267          //index for which (strWidth < maxWidth) or -1
268          right = middle - 1;
269       else
270          return str;
271    }
272    if (right >= 0)
273       return text.SubString(0, right).Trim() + ellipsis;
274 
275    return wxEmptyString;
276 }
277 
278 
279 #ifdef USE_MIDI
280 #endif // USE_MIDI
281 
282 
UpdateSelectedPrefs(int id)283 void TrackArtist::UpdateSelectedPrefs( int id )
284 {
285    if( id == ShowClippingPrefsID())
286       mShowClipping = gPrefs->Read(wxT("/GUI/ShowClipping"), mShowClipping);
287    if( id == ShowTrackNameInWaveformPrefsID())
288       mbShowTrackNameInTrack = gPrefs->ReadBool(wxT("/GUI/ShowTrackNameInWaveform"), false);
289 }
290 
UpdatePrefs()291 void TrackArtist::UpdatePrefs()
292 {
293    mdBrange = DecibelScaleCutoff.Read();
294    mSampleDisplay = TracksPrefs::SampleViewChoice();
295 
296    UpdateSelectedPrefs( ShowClippingPrefsID() );
297    UpdateSelectedPrefs( ShowTrackNameInWaveformPrefsID() );
298 
299    SetColours(0);
300 }
301 
DrawClipAffordance(wxDC & dc,const wxRect & rect,const wxString & title,bool highlight,bool selected)302 void TrackArt::DrawClipAffordance(wxDC& dc, const wxRect& rect, const wxString& title, bool highlight, bool selected)
303 {
304    //To make sure that roundings do not overlap each other
305    auto clipFrameRadius = std::min(ClipFrameRadius, rect.width / 2);
306 
307    wxRect clipRect;
308    bool hasClipRect = dc.GetClippingBox(clipRect);
309    //Fix #1689: visual glitches appear on attempt to draw a rectangle
310    //larger than 0x7FFFFFF pixels wide (value was discovered
311    //by manual testing, and maybe depends on OS being used), but
312    //it's very unlikely that such huge rectangle will be ever fully visible
313    //on the screen, so we can safely reduce its size to be slightly larger than
314    //clipping rectangle, and avoid that problem
315    auto drawingRect = rect;
316    if (hasClipRect)
317    {
318        //to make sure that rounding happends outside the clipping rectangle
319        drawingRect.SetLeft(std::max(rect.GetLeft(), clipRect.GetLeft() - clipFrameRadius - 1));
320        drawingRect.SetRight(std::min(rect.GetRight(), clipRect.GetRight() + clipFrameRadius + 1));
321    }
322 
323    if (selected)
324    {
325       wxRect strokeRect{
326          drawingRect.x - ClipSelectionStrokeSize,
327          drawingRect.y,
328          drawingRect.width + ClipSelectionStrokeSize * 2,
329          drawingRect.height + clipFrameRadius };
330       dc.SetBrush(*wxTRANSPARENT_BRUSH);
331       AColor::UseThemeColour(&dc, clrClipAffordanceStroke, clrClipAffordanceStroke);
332       dc.DrawRoundedRectangle(strokeRect, clipFrameRadius);
333    }
334 
335    AColor::UseThemeColour(&dc, highlight ? clrClipAffordanceActiveBrush : clrClipAffordanceInactiveBrush, clrClipAffordanceOutlinePen);
336    dc.DrawRoundedRectangle(
337       wxRect(
338          drawingRect.x,
339          drawingRect.y + ClipSelectionStrokeSize,
340          drawingRect.width,
341          drawingRect.height + clipFrameRadius
342       ), clipFrameRadius
343    );
344 
345    if (!title.empty())
346    {
347       auto titleRect = hasClipRect ?
348          //avoid drawing text outside the clipping rectangle if possible
349          TrackArt::GetAffordanceTitleRect(rect.Intersect(clipRect)) :
350          TrackArt::GetAffordanceTitleRect(rect);
351 
352       auto truncatedTitle = TrackArt::TruncateText(dc, title, titleRect.GetWidth());
353       if (!truncatedTitle.empty())
354       {
355           auto hAlign = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? wxALIGN_RIGHT : wxALIGN_LEFT;
356           dc.DrawLabel(truncatedTitle, titleRect, hAlign | wxALIGN_CENTER_VERTICAL);
357       }
358    }
359 }
360 
GetAffordanceTitleRect(const wxRect & rect)361 AUDACITY_DLL_API wxRect TrackArt::GetAffordanceTitleRect(const wxRect& rect)
362 {
363     constexpr int FrameThickness{ 1 };
364     return wxRect(
365         rect.GetLeft() + ClipFrameRadius,
366         rect.GetTop() + ClipSelectionStrokeSize + FrameThickness,
367         rect.GetWidth() - ClipFrameRadius * 2,
368         rect.GetHeight() - ClipSelectionStrokeSize - FrameThickness);
369 }
370 
DrawClipEdges(wxDC & dc,const wxRect & clipRect,bool selected)371 void TrackArt::DrawClipEdges(wxDC& dc, const wxRect& clipRect, bool selected)
372 {
373    dc.SetBrush(*wxTRANSPARENT_BRUSH);
374    {
375       AColor::UseThemeColour(&dc, -1, clrClipAffordanceOutlinePen);
376       AColor::Line(dc,
377          clipRect.GetLeft(), clipRect.GetTop(),
378          clipRect.GetLeft(), clipRect.GetBottom());
379       AColor::Line(dc,
380          clipRect.GetRight(), clipRect.GetTop(),
381          clipRect.GetRight(), clipRect.GetBottom());
382    }
383    if(selected)
384    {
385       if constexpr (ClipSelectionStrokeSize == 1)
386       {
387          AColor::UseThemeColour(&dc, -1, clrClipAffordanceStroke);
388          AColor::Line(dc,
389             clipRect.GetLeft() - ClipSelectionStrokeSize, clipRect.GetTop(),
390             clipRect.GetLeft() - ClipSelectionStrokeSize, clipRect.GetBottom());
391          AColor::Line(dc,
392             clipRect.GetRight() + ClipSelectionStrokeSize, clipRect.GetTop(),
393             clipRect.GetRight() + ClipSelectionStrokeSize, clipRect.GetBottom());
394       }
395       else if constexpr (ClipSelectionStrokeSize > 1)
396       {
397          AColor::UseThemeColour(&dc, clrClipAffordanceStroke, clrClipAffordanceStroke);
398          dc.DrawRectangle(wxRect(
399             clipRect.GetLeft() - ClipSelectionStrokeSize, clipRect.GetTop(),
400             ClipSelectionStrokeSize, clipRect.GetHeight()));
401          dc.DrawRectangle(wxRect(
402             clipRect.GetRight() + 1, clipRect.GetTop(),
403             ClipSelectionStrokeSize, clipRect.GetHeight()));
404       }
405    }
406 }
407 
DrawClipFolded(wxDC & dc,const wxRect & rect)408 void TrackArt::DrawClipFolded(wxDC& dc, const wxRect& rect)
409 {
410    AColor::UseThemeColour(&dc, clrClipAffordanceOutlinePen);
411    dc.DrawRectangle(rect);
412 }
413 
414 // Draws the sync-lock bitmap, tiled; always draws stationary relative to the DC
415 //
416 // AWD: now that the tiles don't link together, we're drawing a tilted grid, at
417 // two steps down for every one across. This creates a pattern that repeats in
418 // 5-step by 5-step boxes. Because we're only drawing in 5/25 possible positions
419 // we have a grid spacing somewhat smaller than the image dimensions. Thus we
420 // achieve lower density than with a square grid and eliminate edge cases where
421 // no tiles are displayed.
422 //
423 // The pattern draws in tiles at (0,0), (2,1), (4,2), (1,3), and (3,4) in each
424 // 5x5 box.
425 //
426 // There may be a better way to do this, or a more appealing pattern.
DrawSyncLockTiles(TrackPanelDrawingContext & context,const wxRect & rect)427 void TrackArt::DrawSyncLockTiles(
428    TrackPanelDrawingContext &context, const wxRect &rect )
429 {
430    const auto dc = &context.dc;
431 
432    wxBitmap syncLockBitmap(theTheme.Image(bmpSyncLockSelTile));
433 
434    // Grid spacing is a bit smaller than actual image size
435    int gridW = syncLockBitmap.GetWidth() - 6;
436    int gridH = syncLockBitmap.GetHeight() - 8;
437 
438    // Horizontal position within the grid, modulo its period
439    int blockX = (rect.x / gridW) % 5;
440 
441    // Amount to offset drawing of first column
442    int xOffset = rect.x % gridW;
443    if (xOffset < 0) xOffset += gridW;
444 
445    // Check if we're missing an extra column to the left (this can happen
446    // because the tiles are bigger than the grid spacing)
447    bool extraCol = false;
448    if (syncLockBitmap.GetWidth() - gridW > xOffset) {
449       extraCol = true;
450       xOffset += gridW;
451       blockX = (blockX - 1) % 5;
452    }
453    // Make sure blockX is non-negative
454    if (blockX < 0) blockX += 5;
455 
456    int xx = 0;
457    while (xx < rect.width) {
458       int width = syncLockBitmap.GetWidth() - xOffset;
459       if (xx + width > rect.width)
460          width = rect.width - xx;
461 
462       //
463       // Draw each row in this column
464       //
465 
466       // Vertical position in the grid, modulo its period
467       int blockY = (rect.y / gridH) % 5;
468 
469       // Amount to offset drawing of first row
470       int yOffset = rect.y % gridH;
471       if (yOffset < 0) yOffset += gridH;
472 
473       // Check if we're missing an extra row on top (this can happen because
474       // the tiles are bigger than the grid spacing)
475       bool extraRow = false;
476       if (syncLockBitmap.GetHeight() - gridH > yOffset) {
477          extraRow = true;
478          yOffset += gridH;
479          blockY = (blockY - 1) % 5;
480       }
481       // Make sure blockY is non-negative
482       if (blockY < 0) blockY += 5;
483 
484       int yy = 0;
485       while (yy < rect.height)
486       {
487          int height = syncLockBitmap.GetHeight() - yOffset;
488          if (yy + height > rect.height)
489             height = rect.height - yy;
490 
491          // AWD: draw blocks according to our pattern
492          if ((blockX == 0 && blockY == 0) || (blockX == 2 && blockY == 1) ||
493              (blockX == 4 && blockY == 2) || (blockX == 1 && blockY == 3) ||
494              (blockX == 3 && blockY == 4))
495          {
496 
497             // Do we need to get a sub-bitmap?
498             if (width != syncLockBitmap.GetWidth() || height != syncLockBitmap.GetHeight()) {
499                wxBitmap subSyncLockBitmap =
500                   syncLockBitmap.GetSubBitmap(wxRect(xOffset, yOffset, width, height));
501                dc->DrawBitmap(subSyncLockBitmap, rect.x + xx, rect.y + yy, true);
502             }
503             else {
504                dc->DrawBitmap(syncLockBitmap, rect.x + xx, rect.y + yy, true);
505             }
506          }
507 
508          // Updates for next row
509          if (extraRow) {
510             // Second offset row, still at y = 0; no more extra rows
511             yOffset -= gridH;
512             extraRow = false;
513          }
514          else {
515             // Move on in y, no more offset rows
516             yy += gridH - yOffset;
517             yOffset = 0;
518          }
519          blockY = (blockY + 1) % 5;
520       }
521 
522       // Updates for next column
523       if (extraCol) {
524          // Second offset column, still at x = 0; no more extra columns
525          xOffset -= gridW;
526          extraCol = false;
527       }
528       else {
529          // Move on in x, no more offset rows
530          xx += gridW - xOffset;
531          xOffset = 0;
532       }
533       blockX = (blockX + 1) % 5;
534    }
535 }
536 
DrawBackgroundWithSelection(TrackPanelDrawingContext & context,const wxRect & rect,const Track * track,const wxBrush & selBrush,const wxBrush & unselBrush,bool useSelection)537 void TrackArt::DrawBackgroundWithSelection(
538    TrackPanelDrawingContext &context, const wxRect &rect,
539    const Track *track, const wxBrush &selBrush, const wxBrush &unselBrush,
540    bool useSelection)
541 {
542    const auto dc = &context.dc;
543    const auto artist = TrackArtist::Get( context );
544    const auto &selectedRegion = *artist->pSelectedRegion;
545    const auto &zoomInfo = *artist->pZoomInfo;
546 
547    //MM: Draw background. We should optimize that a bit more.
548    const double sel0 = useSelection ? selectedRegion.t0() : 0.0;
549    const double sel1 = useSelection ? selectedRegion.t1() : 0.0;
550 
551    dc->SetPen(*wxTRANSPARENT_PEN);
552    if (track->GetSelected() || track->IsSyncLockSelected())
553    {
554       // Rectangles before, within, after the selection
555       wxRect before = rect;
556       wxRect within = rect;
557       wxRect after = rect;
558 
559       before.width = (int)(zoomInfo.TimeToPosition(sel0) );
560       if (before.GetRight() > rect.GetRight()) {
561          before.width = rect.width;
562       }
563 
564       if (before.width > 0) {
565          dc->SetBrush(unselBrush);
566          dc->DrawRectangle(before);
567 
568          within.x = 1 + before.GetRight();
569       }
570       within.width = rect.x + (int)(zoomInfo.TimeToPosition(sel1) ) - within.x -1;
571 
572       if (within.GetRight() > rect.GetRight()) {
573          within.width = 1 + rect.GetRight() - within.x;
574       }
575 
576       // Bug 2389 - Selection can disappear
577       // This handles case where no waveform is visible.
578       if (within.width < 1)
579       {
580          within.width = 1;
581       }
582 
583       if (within.width > 0) {
584          if (track->GetSelected()) {
585             dc->SetBrush(selBrush);
586             dc->DrawRectangle(within);
587          }
588          else {
589             // Per condition above, track must be sync-lock selected
590             dc->SetBrush(unselBrush);
591             dc->DrawRectangle(within);
592             DrawSyncLockTiles( context, within );
593          }
594 
595          after.x = 1 + within.GetRight();
596       }
597       else {
598          // `within` not drawn; start where it would have gone
599          after.x = within.x;
600       }
601 
602       after.width = 1 + rect.GetRight() - after.x;
603       if (after.width > 0) {
604          dc->SetBrush(unselBrush);
605          dc->DrawRectangle(after);
606       }
607    }
608    else
609    {
610       // Track not selected; just draw background
611       dc->SetBrush(unselBrush);
612       dc->DrawRectangle(rect);
613    }
614 }
615 
DrawCursor(TrackPanelDrawingContext & context,const wxRect & rect,const Track * track)616 void TrackArt::DrawCursor(TrackPanelDrawingContext& context,
617    const wxRect& rect, const Track* track)
618 {
619    const auto dc = &context.dc;
620    const auto artist = TrackArtist::Get(context);
621    const auto& selectedRegion = *artist->pSelectedRegion;
622 
623    if (selectedRegion.isPoint())
624    {
625        const auto& zoomInfo = *artist->pZoomInfo;
626        auto x = static_cast<int>(zoomInfo.TimeToPosition(selectedRegion.t0(), rect.x));
627        if (x >= rect.GetLeft() && x <= rect.GetRight())
628        {
629           AColor::CursorColor(dc);
630           AColor::Line(*dc, x, rect.GetTop(), x, rect.GetBottom());
631        }
632    }
633 }
634 
635