windowcapture
исходный код / UI/EditorForm.GlassButtons.cs

EditorForm.GlassButtons.cs

489 строк · 19,536 байт · модуль UI
  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}