@@ -18,14 +18,14 @@ public class ThemePreviewer : SKControl
1818 private const int ANIMATION_FPS = 120 ;
1919 private const int MARGIN_STANDARD = 20 ;
2020 private const int BORDER_RADIUS = 5 ;
21+ private const int ARROW_AREA_WIDTH = 80 ;
2122 private const byte OVERLAY_ALPHA = 127 ;
2223 private const float OPACITY_NORMAL = 0.5f ;
2324 private const float OPACITY_HOVER = 1.0f ;
2425 private const float OPACITY_MESSAGE = 0.8f ;
2526
2627 public ThemePreviewerViewModel ViewModel { get ; }
2728
28- private readonly Timer animationTimer ;
2929 private readonly Timer fadeTimer ;
3030 private float fadeProgress = 0f ;
3131 private bool isAnimating = false ;
@@ -41,12 +41,19 @@ public class ThemePreviewer : SKControl
4141 private readonly SKFont iconFont20 ;
4242 private readonly SKSamplingOptions samplingOptions = new SKSamplingOptions ( SKCubicResampler . Mitchell ) ;
4343
44- private Point mousePosition ;
4544 private bool isMouseOverPlay = false ;
4645 private bool isMouseOverLeft = false ;
4746 private bool isMouseOverRight = false ;
4847 private bool isMouseOverDownload = false ;
4948
49+ // Cached UI element rectangles for rendering and hit testing
50+ private Rectangle titleBoxRect ;
51+ private Rectangle playButtonRect ;
52+ private Rectangle downloadMessageRect ;
53+ private Rectangle authorLabelRect ;
54+ private Rectangle downloadSizeLabelRect ;
55+ private Rectangle [ ] carouselIndicatorRects ;
56+
5057 public ThemePreviewer ( )
5158 {
5259 ViewModel = new ThemePreviewerViewModel ( StartAnimation , StopAnimation ) ;
@@ -76,12 +83,6 @@ public ThemePreviewer()
7683 } ;
7784 fadeTimer . Tick += FadeTimer_Tick ;
7885
79- // Timer for auto-advance
80- animationTimer = new Timer
81- {
82- Interval = 1000 / 60
83- } ;
84-
8586 MouseEnter += ( s , e ) => ViewModel . IsMouseOver = true ;
8687 MouseLeave += ( s , e ) => ViewModel . IsMouseOver = false ;
8788
@@ -182,65 +183,61 @@ private void DrawOverlay(SKCanvas canvas, SKImageInfo info)
182183 DrawArrowArea ( canvas , info , false ) ;
183184 }
184185
185- // Title and preview text box (top left) - Border with Margin=20, StackPanel with Margin=10
186- basePaint . Color = new SKColor ( 0 , 0 , 0 , 127 ) ;
187-
188- // Measure text to calculate proper box size
186+ // Title and preview text box (top left)
189187 var titleBounds = new SKRect ( ) ;
190188 titleFont . MeasureText ( ViewModel . Title ?? "" , out titleBounds ) ;
191-
192189 var previewBounds = new SKRect ( ) ;
193190 previewFont . MeasureText ( ViewModel . PreviewText ?? "" , out previewBounds ) ;
194191
195- float boxWidth = Math . Max ( titleBounds . Width , previewBounds . Width ) + 20 ; // 10 margin each side
196- float boxHeight = 19 + 4 + 16 + 20 ; // title size + margin + preview size + top/bottom margin
192+ float boxWidth = Math . Max ( titleBounds . Width , previewBounds . Width ) + 20 ;
193+ float boxHeight = 19 + 4 + 16 + 20 ;
194+ titleBoxRect = new Rectangle ( 20 , 20 , ( int ) boxWidth , ( int ) boxHeight ) ;
197195
198- var titleRect = SKRect . Create ( 20 , 20 , boxWidth , boxHeight ) ;
199196 basePaint . Color = new SKColor ( 0 , 0 , 0 , 127 ) ;
200- canvas . DrawRoundRect ( titleRect , 5 , 5 , basePaint ) ;
197+ canvas . DrawRoundRect ( SKRect . Create ( titleBoxRect . X , titleBoxRect . Y , titleBoxRect . Width , titleBoxRect . Height ) , 5 , 5 , basePaint ) ;
201198
202- // Title text - 10px margin from border
203199 basePaint . Color = SKColors . White ;
204- canvas . DrawText ( ViewModel . Title ?? "" , 30 , 20 + 10 + 19 , titleFont , basePaint ) ; // margin + top padding + font size
205-
206- // Preview text - 4px below title, 16px font
207- canvas . DrawText ( ViewModel . PreviewText ?? "" , 30 , 20 + 10 + 19 + 4 + 16 , previewFont , basePaint ) ; // add 4px margin + 16px for text
200+ canvas . DrawText ( ViewModel . Title ?? "" , titleBoxRect . X + 10 , titleBoxRect . Y + 8 + 19 , titleFont , basePaint ) ;
201+ canvas . DrawText ( ViewModel . PreviewText ?? "" , titleBoxRect . X + 10 , titleBoxRect . Y + 8 + 19 + 5 + 16 , previewFont , basePaint ) ;
208202
209- // Play/Pause button (top right) - MinWidth=40, MinHeight=40, Margin=20
203+ // Play/Pause button (top right)
204+ playButtonRect = new Rectangle ( info . Width - 40 - 20 , 20 , 40 , 40 ) ;
205+
210206 basePaint . Color = new SKColor ( 0 , 0 , 0 , 127 ) ;
211- var playButtonRect = SKRect . Create ( info . Width - 40 - 20 , 20 , 40 , 40 ) ;
212- canvas . DrawRoundRect ( playButtonRect , 5 , 5 , basePaint ) ;
207+ canvas . DrawRoundRect ( SKRect . Create ( playButtonRect . X , playButtonRect . Y , playButtonRect . Width , playButtonRect . Height ) , 5 , 5 , basePaint ) ;
213208
214209 float playOpacity = isMouseOverPlay ? 1.0f : 0.5f ;
215210 basePaint . Color = SKColors . White . WithAlpha ( ( byte ) ( 255 * playOpacity ) ) ;
216211 string playIcon = ViewModel . IsPlaying ? "\uf04c " : "\uf04b " ;
217212 var textBounds = new SKRect ( ) ;
218213 iconFont16 . MeasureText ( playIcon , out textBounds ) ;
219- float centerX = info . Width - 20 - 20 ;
220- float centerY = 20 + 20 ;
214+ float centerX = playButtonRect . X + playButtonRect . Width / 2 ;
215+ float centerY = playButtonRect . Y + playButtonRect . Height / 2 ;
221216 canvas . DrawText ( playIcon , centerX - textBounds . MidX , centerY - textBounds . MidY , iconFont16 , basePaint ) ;
222217
223218 // Corner labels
224219 DrawCornerLabel ( canvas , info , ViewModel . Author , isBottomRight : true ) ;
225220 DrawCornerLabel ( canvas , info , ViewModel . DownloadSize , isBottomRight : false ) ;
226221
227- // Download message (centered bottom) - Margin="0,0,0,15", TextBlock Margin="8,6,8,6"
222+ // Download message (centered bottom)
228223 if ( ! string . IsNullOrEmpty ( ViewModel . Message ) )
229224 {
230225 var msgBounds = new SKRect ( ) ;
231226 textFont . MeasureText ( ViewModel . Message , out msgBounds ) ;
232- // TextBlock margin: 8,6,8,6 = left+right=16, top+bottom=12
233227 float msgWidth = msgBounds . Width + 16 ;
234- float msgHeight = 6 + 16 + 6 ; // top margin + text height + bottom margin
235- var msgRect = SKRect . Create ( info . Width / 2 - msgWidth / 2 , info . Height - msgHeight - 15 , msgWidth , msgHeight ) ;
228+ float msgHeight = 6 + 16 + 6 ;
229+ downloadMessageRect = new Rectangle ( ( int ) ( info . Width / 2 - msgWidth / 2 ) , info . Height - ( int ) msgHeight - 15 , ( int ) msgWidth , ( int ) msgHeight ) ;
236230
237231 basePaint . Color = new SKColor ( 0 , 0 , 0 , 127 ) ;
238- canvas . DrawRoundRect ( msgRect , 5 , 5 , basePaint ) ;
232+ canvas . DrawRoundRect ( SKRect . Create ( downloadMessageRect . X , downloadMessageRect . Y , downloadMessageRect . Width , downloadMessageRect . Height ) , 5 , 5 , basePaint ) ;
239233
240234 float msgOpacity = isMouseOverDownload ? 1.0f : 0.8f ;
241235 basePaint . Color = SKColors . White . WithAlpha ( ( byte ) ( 255 * msgOpacity ) ) ;
242- // Text positioned: border top + top margin + font baseline
243- canvas . DrawText ( ViewModel . Message , info . Width / 2 - msgBounds . Width / 2 , info . Height - msgHeight - 15 + 6 + 16 , textFont , basePaint ) ;
236+ canvas . DrawText ( ViewModel . Message , downloadMessageRect . X + 8 , downloadMessageRect . Y + 5 + 16 , textFont , basePaint ) ;
237+ }
238+ else
239+ {
240+ downloadMessageRect = Rectangle . Empty ;
244241 }
245242
246243 // Carousel indicators - Margin=16, Height=32, Rectangle Height=3, Width=30, Margin="3,0"
@@ -268,40 +265,56 @@ private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, bool isLeft)
268265
269266 private void DrawCornerLabel ( SKCanvas canvas , SKImageInfo info , string text , bool isBottomRight )
270267 {
271- if ( string . IsNullOrEmpty ( text ) ) return ;
268+ if ( string . IsNullOrEmpty ( text ) )
269+ {
270+ if ( isBottomRight )
271+ authorLabelRect = Rectangle . Empty ;
272+ else
273+ downloadSizeLabelRect = Rectangle . Empty ;
274+ return ;
275+ }
272276
273- basePaint . Color = new SKColor ( 0 , 0 , 0 , OVERLAY_ALPHA ) ;
274277 var textBounds = new SKRect ( ) ;
275278 textFont . MeasureText ( text , out textBounds ) ;
276279
277280 // Calculate margins and dimensions
278281 float leftMargin = isBottomRight ? 8 : 11 ;
279282 float rightMargin = isBottomRight ? 11 : 8 ;
280283 float borderWidth = textBounds . Width + leftMargin + rightMargin ;
281- float borderHeight = 4 + 16 + 9 ; // top + text + bottom
284+ float borderHeight = 4 + 16 + 9 ;
282285
283- // Position rect
286+ // Position rect and cache it
284287 float rectX = isBottomRight ? info . Width - borderWidth + 3 : - 3 ;
285288 float rectY = info . Height - borderHeight + 3 ;
286- var rect = SKRect . Create ( rectX , rectY , borderWidth , borderHeight ) ;
287- canvas . DrawRoundRect ( rect , BORDER_RADIUS , BORDER_RADIUS , basePaint ) ;
289+ Rectangle labelRect = new Rectangle ( ( int ) rectX , ( int ) rectY , ( int ) borderWidth , ( int ) borderHeight ) ;
290+
291+ if ( isBottomRight )
292+ authorLabelRect = labelRect ;
293+ else
294+ downloadSizeLabelRect = labelRect ;
295+
296+ basePaint . Color = new SKColor ( 0 , 0 , 0 , OVERLAY_ALPHA ) ;
297+ canvas . DrawRoundRect ( SKRect . Create ( rectX , rectY , borderWidth , borderHeight ) , BORDER_RADIUS , BORDER_RADIUS , basePaint ) ;
288298
289299 // Draw text
290300 basePaint . Color = SKColors . White . WithAlpha ( OVERLAY_ALPHA ) ;
291301 float textX = isBottomRight ? info . Width - textBounds . Width - rightMargin + 3 : leftMargin - 3 ;
292- float textY = rectY + 4 + 16 ; // top margin + baseline
302+ float textY = rectY + 4 + 16 ;
293303 canvas . DrawText ( text , textX , textY , textFont , basePaint ) ;
294304 }
295305
296306 private void DrawCarouselIndicators ( SKCanvas canvas , SKImageInfo info )
297307 {
298308 int count = ViewModel . Items . Count ;
299- int indicatorWidth = 30 ; // Rectangle Width=30
300- int indicatorHeight = 3 ; // Rectangle Height=3
301- int itemSpacing = 6 ; // Margin="3,0" means 3px on each side = 6px spacing
309+ int indicatorWidth = 30 ;
310+ int indicatorHeight = 3 ;
311+ int itemSpacing = 6 ;
302312 int totalWidth = count * indicatorWidth + ( count - 1 ) * itemSpacing ;
303313 int startX = ( info . Width - totalWidth ) / 2 ;
304- int y = info . Height - 16 - 32 / 2 ; // Margin=16 from bottom, Height=32, centered vertically
314+ int y = info . Height - 16 - 32 / 2 ;
315+
316+ // Cache clickable rectangles for hit testing
317+ carouselIndicatorRects = new Rectangle [ count ] ;
305318
306319 for ( int i = 0 ; i < count ; i ++ )
307320 {
@@ -311,6 +324,9 @@ private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info)
311324 int rectX = startX + i * ( indicatorWidth + itemSpacing ) ;
312325 var rect = SKRect . Create ( rectX , y - indicatorHeight / 2 , indicatorWidth , indicatorHeight ) ;
313326 canvas . DrawRect ( rect , basePaint ) ;
327+
328+ // Cache full clickable height for hit testing
329+ carouselIndicatorRects [ i ] = new Rectangle ( rectX , y - 16 , indicatorWidth , 32 ) ;
314330 }
315331 }
316332
@@ -320,61 +336,40 @@ protected override void OnMouseClick(MouseEventArgs e)
320336
321337 if ( ! ViewModel . ControlsVisible ) return ;
322338
323- // Check if play button was clicked - MinWidth=40, MinHeight=40, Margin=20
324- var playButtonRect = new Rectangle ( Width - 40 - 20 , 20 , 40 , 40 ) ;
339+ // Check if play button was clicked
325340 if ( playButtonRect . Contains ( e . Location ) )
326341 {
327342 ViewModel . TogglePlayPause ( ) ;
328343 return ;
329344 }
330345
331346 // Check if left arrow area was clicked
332- if ( e . X < 80 )
347+ if ( e . X < ARROW_AREA_WIDTH )
333348 {
334349 ViewModel . Previous ( ) ;
335350 return ;
336351 }
337352
338353 // Check if right arrow area was clicked
339- if ( e . X > Width - 80 )
354+ if ( e . X > Width - ARROW_AREA_WIDTH )
340355 {
341356 ViewModel . Next ( ) ;
342357 return ;
343358 }
344359
345- // Check if download message was clicked - Margin="0,0,0,15", TextBlock Margin="8,6,8,6"
346- if ( ! string . IsNullOrEmpty ( ViewModel . Message ) )
360+ // Check if download message was clicked
361+ if ( downloadMessageRect . Contains ( e . Location ) )
347362 {
348- using ( var textFont = new SKFont ( SKTypeface . FromFamilyName ( "Segoe UI" ) , 16 ) )
349- {
350- var msgBounds = new SKRect ( ) ;
351- textFont . MeasureText ( ViewModel . Message , out msgBounds ) ;
352- float msgWidth = msgBounds . Width + 16 ; // 8+8
353- float msgHeight = 6 + 16 + 6 ; // top + text + bottom margin
354- var msgRect = new Rectangle ( ( int ) ( Width / 2 - msgWidth / 2 ) , Height - ( int ) msgHeight - 15 , ( int ) msgWidth , ( int ) msgHeight ) ;
355- if ( msgRect . Contains ( e . Location ) )
356- {
357- ViewModel . InvokeDownload ( ) ;
358- return ;
359- }
360- }
363+ ViewModel . InvokeDownload ( ) ;
364+ return ;
361365 }
362366
363- // Check if carousel indicator was clicked - Margin=16, Height=32
364- if ( ViewModel . CarouselIndicatorsVisible && ViewModel . Items . Count > 0 )
367+ // Check if carousel indicator was clicked
368+ if ( carouselIndicatorRects != null )
365369 {
366- int count = ViewModel . Items . Count ;
367- int indicatorWidth = 30 ;
368- int itemSpacing = 6 ;
369- int totalWidth = count * indicatorWidth + ( count - 1 ) * itemSpacing ;
370- int startX = ( Width - totalWidth ) / 2 ;
371- int y = Height - 16 - 32 / 2 ;
372-
373- for ( int i = 0 ; i < count ; i ++ )
370+ for ( int i = 0 ; i < carouselIndicatorRects . Length ; i ++ )
374371 {
375- int rectX = startX + i * ( indicatorWidth + itemSpacing ) ;
376- var rect = new Rectangle ( rectX , y - 16 , indicatorWidth , 32 ) ; // Full clickable height
377- if ( rect . Contains ( e . Location ) )
372+ if ( carouselIndicatorRects [ i ] . Contains ( e . Location ) )
378373 {
379374 ViewModel . SelectedIndex = i ;
380375 return ;
@@ -387,7 +382,6 @@ protected override void OnMouseMove(MouseEventArgs e)
387382 {
388383 base . OnMouseMove ( e ) ;
389384
390- mousePosition = e . Location ;
391385 bool needsRedraw = false ;
392386
393387 // Update cursor based on location
@@ -402,35 +396,52 @@ protected override void OnMouseMove(MouseEventArgs e)
402396 bool wasOverRight = isMouseOverRight ;
403397 bool wasOverDownload = isMouseOverDownload ;
404398
405- // Play button - MinWidth=40, MinHeight=40, Margin=20
406- var playButtonRect = new Rectangle ( Width - 40 - 20 , 20 , 40 , 40 ) ;
399+ // Reset all hover states
400+ isMouseOverPlay = false ;
401+ isMouseOverLeft = false ;
402+ isMouseOverRight = false ;
403+ isMouseOverDownload = false ;
404+
405+ // Check UI elements in priority order using cached rectangles
407406 isMouseOverPlay = playButtonRect . Contains ( e . Location ) ;
407+
408+ if ( ! isMouseOverPlay )
409+ isMouseOverDownload = downloadMessageRect . Contains ( e . Location ) ;
408410
409- // Left arrow (80px wide area on left side, full height)
410- isMouseOverLeft = e . X < 80 ;
411+ // Check if over any UI box (title, corner labels)
412+ bool isOverUIBox = false ;
413+ if ( ! isMouseOverPlay && ! isMouseOverDownload )
414+ {
415+ isOverUIBox = titleBoxRect . Contains ( e . Location ) ||
416+ authorLabelRect . Contains ( e . Location ) ||
417+ downloadSizeLabelRect . Contains ( e . Location ) ;
418+ }
411419
412- // Right arrow (80px wide area on right side, full height)
413- isMouseOverRight = e . X > Width - 80 ;
420+ // Arrows only if not over other UI elements
421+ if ( ! isMouseOverPlay && ! isMouseOverDownload && ! isOverUIBox )
422+ {
423+ isMouseOverLeft = e . X < ARROW_AREA_WIDTH ;
424+ isMouseOverRight = e . X > Width - ARROW_AREA_WIDTH ;
425+ }
414426
415- // Download message
416- isMouseOverDownload = false ;
417- if ( ! string . IsNullOrEmpty ( ViewModel . Message ) )
427+ // Check carousel indicators for hand cursor
428+ bool isOverCarouselIndicator = false ;
429+ if ( carouselIndicatorRects != null )
418430 {
419- using ( var textFont = new SKFont ( SKTypeface . FromFamilyName ( "Segoe UI" ) , 16 ) )
431+ for ( int i = 0 ; i < carouselIndicatorRects . Length ; i ++ )
420432 {
421- var msgBounds = new SKRect ( ) ;
422- textFont . MeasureText ( ViewModel . Message , out msgBounds ) ;
423- float msgWidth = msgBounds . Width + 16 ; // 8+8
424- float msgHeight = 6 + 16 + 6 ; // top + text + bottom margin
425- var msgRect = new Rectangle ( ( int ) ( Width / 2 - msgWidth / 2 ) , Height - ( int ) msgHeight - 15 , ( int ) msgWidth , ( int ) msgHeight ) ;
426- isMouseOverDownload = msgRect . Contains ( e . Location ) ;
433+ if ( carouselIndicatorRects [ i ] . Contains ( e . Location ) )
434+ {
435+ isOverCarouselIndicator = true ;
436+ break ;
437+ }
427438 }
428439 }
429440
430441 needsRedraw = ( isMouseOverPlay != wasOverPlay ) || ( isMouseOverLeft != wasOverLeft ) ||
431442 ( isMouseOverRight != wasOverRight ) || ( isMouseOverDownload != wasOverDownload ) ;
432443
433- bool isOverClickable = isMouseOverPlay || isMouseOverLeft || isMouseOverRight || isMouseOverDownload ;
444+ bool isOverClickable = isMouseOverPlay || isMouseOverLeft || isMouseOverRight || isMouseOverDownload || isOverCarouselIndicator ;
434445 Cursor = isOverClickable ? Cursors . Hand : Cursors . Default ;
435446
436447 if ( needsRedraw )
@@ -478,7 +489,6 @@ protected override void Dispose(bool disposing)
478489 if ( disposing )
479490 {
480491 fadeTimer ? . Dispose ( ) ;
481- animationTimer ? . Dispose ( ) ;
482492 ViewModel ? . Stop ( ) ;
483493 }
484494 base . Dispose ( disposing ) ;
0 commit comments