1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Drawing2D; 5using System.Drawing.Imaging; 6using System.Drawing.Text; 7using System.IO; 8using System.Linq; 9using System.Runtime.InteropServices; 10using System.Windows.Forms; 11using WindowCapture.App; 12using WindowCapture.Detection; 13using WindowCapture.Effects; 14using WindowCapture.Helpers; 15using WindowCapture.Integration; 16using WindowCapture.Models; 17using WindowCapture.Native; 18 19namespace WindowCapture.UI 20{ 21 public partial class EditorForm 22 { 23 // ===== Glass hover buttons ===== 24 25 // ====== Glass Hover Buttons (cursor-proximity edge lighting) ====== 26 27 private void InitGlassButtons() 28 { 29 hoverBtnGlow = new float[4]; 30 hoverBtnHovered = new bool[4]; 31 hoverBtnActions = new Action[] 32 { 33 () => ShowSettings(), // [0] gear 34 () => WindowState = FormWindowState.Minimized, // [1] minimize 35 () => { // [2] maximize 36 WindowState = WindowState == FormWindowState.Maximized 37 ? FormWindowState.Normal : FormWindowState.Maximized; 38 }, 39 () => Close() // [3] close 40 }; 41 42 // Timer to detect cursor leaving window (60ms polling) 43 glassLeaveTimer = new System.Windows.Forms.Timer(); 44 glassLeaveTimer.Interval = 60; 45 glassLeaveTimer.Tick += GlassLeaveTimer_Tick; 46 } 47 48 private void GlassLeaveTimer_Tick(object sender, EventArgs e) 49 { 50 // Always track cursor position (even outside window) for smooth glow fade 51 if (scrollContainer == null) return; 52 var screenPt = Cursor.Position; 53 var scPt = scrollContainer.PointToClient(screenPt); 54 UpdateGlassButtons(scPt); 55 } 56 57 private void ClearGlassButtonGlow() 58 { 59 if (hoverBtnGlow == null) return; 60 bool needInvalidate = false; 61 for (int i = 0; i < 4; i++) 62 { 63 if (hoverBtnGlow[i] > 0) 64 { 65 hoverBtnGlow[i] = 0; 66 hoverBtnHovered[i] = false; 67 needInvalidate = true; 68 } 69 } 70 if (needInvalidate) 71 { 72 if (scrollContainer != null && scrollContainer.IsHandleCreated) 73 { 74 scrollContainer.Cursor = Cursors.Default; 75 for (int i = 0; i < 4; i++) 76 { 77 var r = GetGlassBtnRect(i); 78 r.Inflate(15, 15); 79 scrollContainer.Invalidate(r); 80 81 if (canvas != null && canvas.IsHandleCreated) 82 { 83 var cr = new Rectangle(r.X - canvas.Left, r.Y - canvas.Top, r.Width, r.Height); 84 if (canvas.ClientRectangle.IntersectsWith(cr)) 85 canvas.Invalidate(cr); 86 } 87 } 88 } 89 } 90 } 91 92 private Rectangle GetGlassBtnRect(int index) 93 { 94 // [3]=close (rightmost), [2]=max, [1]=min, [0]=gear (leftmost) 95 int fromRight = 3 - index; 96 int x = scrollContainer.Width - (HoverBtnW + 4) - fromRight * (HoverBtnW + HoverBtnGap); 97 return new Rectangle(x, 2, HoverBtnW, HoverBtnH); 98 } 99 100 private int HitTestGlassButton(Point scrollContainerPt) 101 { 102 for (int i = 3; i >= 0; i--) // Test close first (rightmost, most common) 103 { 104 var r = GetGlassBtnRect(i); 105 r.Inflate(2, 2); // Slightly expanded hit area 106 if (r.Contains(scrollContainerPt) && hoverBtnGlow[i] > 0.15f) 107 return i; 108 } 109 return -1; 110 } 111 112 public void UpdateGlassButtons(Point scrollContainerPt) 113 { 114 if (hoverBtnGlow == null) return; 115 lastCursorClientPt = scrollContainerPt; 116 117 bool needInvalidate = false; 118 int hovIdx = HitTestGlassButton(scrollContainerPt); 119 120 for (int i = 0; i < 4; i++) 121 { 122 // Calculate distance from cursor to button center 123 var rect = GetGlassBtnRect(i); 124 float cx = rect.X + rect.Width / 2f; 125 float cy = rect.Y + rect.Height / 2f; 126 float dx = scrollContainerPt.X - cx; 127 float dy = scrollContainerPt.Y - cy; 128 float dist = (float)Math.Sqrt(dx * dx + dy * dy); 129 130 float glow; 131 if (dist <= GlowFullRadius) 132 glow = 1.0f; 133 else if (dist >= GlowMaxRadius) 134 glow = 0.0f; 135 else 136 { 137 float t = (dist - GlowFullRadius) / (GlowMaxRadius - GlowFullRadius); 138 glow = 1.0f - t * t; // Quadratic falloff — gentle fade 139 } 140 141 bool wasHovered = hoverBtnHovered[i]; 142 hoverBtnHovered[i] = (hovIdx == i); 143 144 if (Math.Abs(glow - hoverBtnGlow[i]) > 0.003f || wasHovered != hoverBtnHovered[i]) 145 { 146 hoverBtnGlow[i] = glow; 147 needInvalidate = true; 148 } 149 } 150 151 // Update cursor 152 bool overBtn = hovIdx >= 0; 153 if (scrollContainer != null) 154 scrollContainer.Cursor = overBtn ? Cursors.Hand : Cursors.Default; 155 156 if (needInvalidate) 157 { 158 // SURGICAL INVALIDATION: Only repaint where buttons are. 159 // This prevents the "desktop flash" caused by erasing the whole container. 160 for (int i = 0; i < 4; i++) 161 { 162 var r = GetGlassBtnRect(i); 163 r.Inflate(15, 15); // margin for glow/shadow 164 165 if (scrollContainer != null && scrollContainer.IsHandleCreated) 166 scrollContainer.Invalidate(r); 167 168 if (canvas != null && canvas.IsHandleCreated) 169 { 170 // Map scrollContainer coord to canvas coord 171 var cr = new Rectangle(r.X - canvas.Left, r.Y - canvas.Top, r.Width, r.Height); 172 if (canvas.ClientRectangle.IntersectsWith(cr)) 173 canvas.Invalidate(cr); 174 } 175 } 176 } 177 178 // Start leave-detection timer if any glow > 0 179 bool anyGlow = false; 180 for (int i = 0; i < 4; i++) 181 if (hoverBtnGlow[i] > 0.003f) { anyGlow = true; break; } 182 if (anyGlow && glassLeaveTimer != null && !glassLeaveTimer.Enabled) 183 glassLeaveTimer.Start(); 184 else if (!anyGlow && !isVideoFile && !isAudioFile && glassLeaveTimer != null && glassLeaveTimer.Enabled) 185 glassLeaveTimer.Stop(); 186 } 187 188 // Called from AnnotationCanvas.OnMouseMove when canvas covers the window 189 public void UpdateGlassButtonsFromCanvas(Point canvasLocalPt) 190 { 191 UpdateGlassButtons(canvasLocalPt); 192 } 193 194 // Called from AnnotationCanvas for click forwarding 195 public bool TryClickGlassButton(Point canvasLocalPt) 196 { 197 if (scrollContainer == null || hoverBtnGlow == null) return false; 198 // Since canvas is Dock.Fill, canvasLocalPt is already viewport-local 199 int idx = HitTestGlassButton(canvasLocalPt); 200 if (idx >= 0 && hoverBtnGlow[idx] > 0.15f) 201 { 202 hoverBtnActions[idx](); 203 return true; 204 } 205 return false; 206 } 207 208 // Paint all UI overlays on the canvas surface 209 public void PaintUIOnCanvas(Graphics g) 210 { 211 PaintGlassButtonsOnCanvas(g); 212 PaintZoomIndicatorOnCanvas(g); 213 if (viewerMode && !isVideoFile && !isAudioFile) 214 PaintFileNameIndicatorOnCanvas(g); 215 PaintVolumeIndicatorOnCanvas(g); 216 } 217 218 private void PaintGlassButtonsOnCanvas(Graphics g) 219 { 220 if (hoverBtnGlow == null) return; 221 g.SmoothingMode = SmoothingMode.AntiAlias; 222 for (int i = 0; i < 4; i++) 223 { 224 float glow = hoverBtnGlow[i]; 225 if (glow < 0.005f) continue; 226 PaintGlassButton(g, i, glow); 227 } 228 } 229 230 private void PaintZoomIndicatorOnCanvas(Graphics g) 231 { 232 if (zoomIndicatorAlpha <= 0.005f) return; 233 PaintZoomIndicatorAt(g, 0, 0); 234 } 235 236 private void PaintFileNameIndicatorOnCanvas(Graphics g) 237 { 238 if (fileNameAlpha <= 0.005f) return; 239 PaintFileNameIndicator(g); 240 } 241 242 private void PaintVolumeIndicatorOnCanvas(Graphics g) 243 { 244 PaintVolumeIndicator(g); 245 } 246 247 private void PaintZoomIndicatorAt(Graphics g, int offsetX, int offsetY) 248 { 249 float alpha = zoomIndicatorAlpha; 250 int zoomPct = (int)Math.Round(zoomLevel * 100); 251 var imgSize = GetScaledImageSize(); 252 string line1 = zoomPct + "%"; 253 string line2 = imgSize.Width + " × " + imgSize.Height; 254 255 using (var font1 = new Font("Segoe UI", 15f, FontStyle.Regular, GraphicsUnit.Pixel)) 256 using (var font2 = new Font("Segoe UI", 11f, FontStyle.Regular, GraphicsUnit.Pixel)) 257 { 258 var s1 = g.MeasureString(line1, font1); 259 var s2 = g.MeasureString(line2, font2); 260 float boxW = Math.Max(s1.Width, s2.Width) + 20; 261 float boxH = s1.Height + s2.Height + 12; 262 float bx = offsetX + 10; 263 float by = offsetY + 8; 264 265 // Glass fill 266 int fillA = (int)(alpha * 18); 267 using (var brush = new SolidBrush(Color.FromArgb(fillA, 200, 210, 230))) 268 g.FillRectangle(brush, bx, by, boxW, boxH); 269 270 // Glass edges 271 int edgeA = (int)(alpha * 80); 272 Color ec = Color.FromArgb(edgeA, 225, 232, 245); 273 using (var pen = new Pen(ec, 1.0f)) 274 g.DrawRectangle(pen, bx, by, boxW, boxH); 275 276 // Dynamic: sample actual image brightness behind zoom indicator 277 var indicatorRect = new Rectangle((int)bx, (int)by, (int)boxW, (int)boxH); 278 float brightness = SampleImageBrightnessAt(indicatorRect); 279 Color textColor; 280 if (brightness < 0.55f) 281 textColor = Color.FromArgb((int)(alpha * 220), 220, 228, 242); 282 else 283 textColor = Color.FromArgb((int)(alpha * 220), 30, 30, 40); 284 285 Color dimColor = Color.FromArgb((int)(alpha * 140), 286 textColor.R, textColor.G, textColor.B); 287 288 g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; 289 using (var brush1 = new SolidBrush(textColor)) 290 g.DrawString(line1, font1, brush1, bx + 10, by + 5); 291 using (var brush2 = new SolidBrush(dimColor)) 292 g.DrawString(line2, font2, brush2, bx + 10, by + 5 + s1.Height); 293 } 294 } 295 296 private void PaintGlassButtons(Graphics g) 297 { 298 if (hoverBtnGlow == null) return; 299 g.SmoothingMode = SmoothingMode.AntiAlias; 300 301 for (int i = 0; i < 4; i++) 302 { 303 float glow = hoverBtnGlow[i]; 304 if (glow < 0.005f) continue; 305 PaintGlassButton(g, i, glow); 306 } 307 } 308 309 private void PaintGlassButton(Graphics g, int index, float glow) 310 { 311 var rect = GetGlassBtnRect(index); 312 bool hov = hoverBtnHovered[index]; 313 int x = rect.X, y = rect.Y, w = rect.Width, h = rect.Height; 314 // Subtle glass fill 315 int fillAlpha = (int)(glow * (hov ? 30 : 8)); 316 using (var brush = new SolidBrush(Color.FromArgb(fillAlpha, 200, 210, 230))) 317 g.FillRectangle(brush, x, y, w, h); 318 319 // Directional edge lighting 320 float cx = x + w / 2f; 321 float cy = y + h / 2f; 322 float dx = lastCursorClientPt.X - cx; 323 float dy = lastCursorClientPt.Y - cy; 324 float len = (float)Math.Sqrt(dx * dx + dy * dy); 325 if (len < 0.5f) { dx = 0; dy = -1; len = 1; } 326 dx /= len; dy /= len; 327 328 float baseAlpha = glow * (hov ? 210f : 120f); 329 330 float leftF = 0.15f + 0.85f * Math.Max(0f, -dx); 331 float rightF = 0.15f + 0.85f * Math.Max(0f, dx); 332 float topF = 0.15f + 0.85f * Math.Max(0f, -dy); 333 float botF = 0.15f + 0.85f * Math.Max(0f, dy); 334 335 int aL = Math.Min(255, (int)(baseAlpha * leftF)); 336 int aR = Math.Min(255, (int)(baseAlpha * rightF)); 337 int aT = Math.Min(255, (int)(baseAlpha * topF)); 338 int aB = Math.Min(255, (int)(baseAlpha * botF)); 339 340 Color ec = Color.FromArgb(225, 232, 245); 341 342 using (var pen = new Pen(Color.FromArgb(aL, ec), 1.0f)) 343 g.DrawLine(pen, x, y, x, y + h - 1); 344 using (var pen = new Pen(Color.FromArgb(aR, ec), 1.0f)) 345 g.DrawLine(pen, x + w - 1, y, x + w - 1, y + h - 1); 346 using (var pen = new Pen(Color.FromArgb(aT, ec), 1.0f)) 347 g.DrawLine(pen, x, y, x + w - 1, y); 348 using (var pen = new Pen(Color.FromArgb(aB, ec), 1.0f)) 349 g.DrawLine(pen, x, y + h - 1, x + w - 1, y + h - 1); 350 351 // Icon with auto-color and glow-based opacity 352 int iconAlpha = Math.Min(255, (int)(glow * (hov ? 250 : 160))); 353 DrawGlassIcon(g, index, x, y, w, h, iconAlpha); 354 } 355 356 /// <summary>Sample average brightness of the image region behind a viewport rectangle.</summary> 357 private float SampleImageBrightnessAt(Rectangle viewportRect) 358 { 359 if (captured == null || canvasDrawScale < 0.001f) return 0.3f; 360 361 int imgX = (int)((viewportRect.X - canvasDrawX) / canvasDrawScale); 362 int imgY = (int)((viewportRect.Y - canvasDrawY) / canvasDrawScale); 363 int imgW = Math.Max(1, (int)(viewportRect.Width / canvasDrawScale)); 364 int imgH = Math.Max(1, (int)(viewportRect.Height / canvasDrawScale)); 365 366 // Clamp to image bounds 367 if (imgX < 0) imgX = 0; 368 if (imgY < 0) imgY = 0; 369 if (imgX >= captured.Width) return 0.3f; 370 if (imgY >= captured.Height) return 0.3f; 371 if (imgX + imgW > captured.Width) imgW = captured.Width - imgX; 372 if (imgY + imgH > captured.Height) imgH = captured.Height - imgY; 373 if (imgW <= 0 || imgH <= 0) return 0.3f; 374 375 // Sparse sampling (every 4th pixel for speed) 376 float total = 0; int count = 0; 377 for (int sy = imgY; sy < imgY + imgH; sy += 4) 378 for (int sx = imgX; sx < imgX + imgW; sx += 4) 379 { 380 var c = captured.GetPixel(sx, sy); 381 total += (c.R * 0.299f + c.G * 0.587f + c.B * 0.114f) / 255f; 382 count++; 383 } 384 return count > 0 ? total / count : 0.3f; 385 } 386 387 private void DrawGlassIcon(Graphics g, int index, int x, int y, int w, int h, int alpha) 388 { 389 // Dynamic: sample actual image brightness behind this button 390 var btnRect = GetGlassBtnRect(index); 391 float brightness = SampleImageBrightnessAt(btnRect); 392 Color iconColor; 393 if (brightness < 0.55f) 394 iconColor = Color.FromArgb(alpha, 220, 228, 242); // Light icon on dark bg 395 else 396 iconColor = Color.FromArgb(alpha, 30, 30, 40); // Dark icon on light bg 397 398 if (index == 3) // Close (X) 399 { 400 int px = 10, py = 7; 401 using (var pen = new Pen(iconColor, 1.2f)) 402 { 403 g.DrawLine(pen, x + px, y + py, x + w - px, y + h - py); 404 g.DrawLine(pen, x + w - px, y + py, x + px, y + h - py); 405 } 406 } 407 else if (index == 2) // Maximize (rectangle) 408 { 409 int px = 11, py = 7; 410 using (var pen = new Pen(iconColor, 1.0f)) 411 g.DrawRectangle(pen, x + px, y + py, w - px * 2, h - py * 2); 412 } 413 else if (index == 1) // Minimize (horizontal line) 414 { 415 int px = 11; 416 using (var pen = new Pen(iconColor, 1.2f)) 417 g.DrawLine(pen, x + px, y + h / 2, x + w - px, y + h / 2); 418 } 419 else // Gear (index == 0) 420 { 421 float cx = x + w / 2f; 422 float cy = y + h / 2f; 423 float outerR = 5.5f; 424 float innerR = 2.0f; 425 int teeth = 8; 426 427 var points = new List<PointF>(); 428 for (int i = 0; i < teeth * 2; i++) 429 { 430 float angle = (float)(i * Math.PI / teeth); 431 float r = (i % 2 == 0) ? outerR : outerR * 0.65f; 432 points.Add(new PointF(cx + (float)Math.Cos(angle) * r, cy + (float)Math.Sin(angle) * r)); 433 } 434 435 using (var path = new GraphicsPath()) 436 { 437 path.AddPolygon(points.ToArray()); 438 path.AddEllipse(cx - innerR, cy - innerR, innerR * 2, innerR * 2); 439 using (var brush = new SolidBrush(iconColor)) 440 g.FillPath(brush, path); 441 } 442 } 443 } 444 445 private GraphicsPath CreateRoundRectPath(int x, int y, int w, int h, int radius) 446 { 447 var path = new GraphicsPath(); 448 int d = radius * 2; 449 path.AddArc(x, y, d, d, 180, 90); 450 path.AddArc(x + w - d, y, d, d, 270, 90); 451 path.AddArc(x + w - d, y + h - d, d, d, 0, 90); 452 path.AddArc(x, y + h - d, d, d, 90, 90); 453 path.CloseFigure(); 454 return path; 455 } 456 457 protected override void OnMouseMove(MouseEventArgs e) 458 { 459 base.OnMouseMove(e); 460 if (scrollContainer != null) 461 { 462 var scPt = scrollContainer.PointToClient(PointToScreen(e.Location)); 463 UpdateGlassButtons(scPt); 464 } 465 } 466 467 protected override void OnResize(EventArgs e) 468 { 469 base.OnResize(e); 470 if (scrollContainer != null) 471 { 472 // In viewer mode, re-fit image to new viewport size 473 if (viewerMode && !isVideoFile && !isAudioFile && bounds.Width > 0) 474 FitZoomToViewport(); 475 CenterImage(); 476 } 477 // Update rounded corners region 478 ApplyRoundedCorners(); 479 SyncOverlayFormPosition(); 480 Invalidate(); 481 } 482 483 protected override void OnMove(EventArgs e) 484 { 485 base.OnMove(e); 486 SyncOverlayFormPosition(); 487 } 488 } 489}