Skip to content

Commit 1b7289d

Browse files
committed
Improve hitbox testing for theme previewer
1 parent dae9c55 commit 1b7289d

File tree

1 file changed

+106
-96
lines changed

1 file changed

+106
-96
lines changed

src/Skia/ThemePreviewer.cs

Lines changed: 106 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)