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

SearchForm.cs

790 строк · 47,881 байт · модуль 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.Runtime.InteropServices;
  9using System.Threading;
 10using System.Windows.Forms;
 11using WindowCapture.Effects;
 12using WindowCapture.Helpers;
 13using WindowCapture.Models;
 14using WindowCapture.Native;
 15
 16namespace WindowCapture.UI
 17{
 18    public class SearchForm : Form
 19    {
 20        public event Action PanelHidden;
 21
 22        // ===== Layout =====
 23        private const int CompactW = 440;
 24        private const int ExpandedW = 720;
 25        private const int ItemH = 44;
 26        private const int TreeW = 160;
 27        private const int BottomBarH = 100;
 28        private const int DriveBarH = 32;
 29        private const int BreadcrumbH = 28;
 30
 31        // ===== Own D2D context (separate from global D2DRenderer) =====
 32        private ID2D1Factory d2dFactory;
 33        private ID2D1HwndRenderTarget rt; // own render target
 34        private IDWriteFactory dwFactory;
 35        private IDWriteTextFormat fmtName, fmtPath, fmtMeta, fmtChip, fmtSort, fmtTree, fmtDrive;
 36        private ID2D1SolidColorBrush brText, brDim, brAccent, brSelBg, brSelBar, brHover, brBarBg, brSep;
 37        private ID2D1SolidColorBrush brPhotoB, brVideoB, brAudioB, brDocB;
 38        private ID2D1SolidColorBrush brPhotoBg, brVideoBg, brAudioBg, brDocBg;
 39        private ID2D1SolidColorBrush brTreeBg, brDriveBg, brDriveUsed;
 40        private ID2D1SolidColorBrush brFilterAct, brFilterIn, brBg;
 41
 42        // ===== Slide =====
 43        private System.Windows.Forms.Timer animTimer;
 44        private float slideProgress, slideTarget;
 45
 46        // ===== Search =====
 47        private TextBox searchBox;
 48        private System.Windows.Forms.Timer debounceTimer;
 49        private volatile int cancelToken;
 50        private readonly List<SearchResult> results = new List<SearchResult>();
 51        private float scrollOffset, scrollTarget;
 52        private int hoverIndex = -1, selectedIndex;
 53        private string statusText = "";
 54        private int totalFound;
 55
 56        // ===== Filters + Sort =====
 57        private enum FileFilter { All, Photo, Video, Audio, Docs, Recent }
 58        private FileFilter currentFilter = FileFilter.Recent;
 59        private enum SortMode { Name, Size, Type, Date }
 60        private SortMode currentSort = SortMode.Date;
 61        private bool sortDescending = true;
 62        private readonly string[] FilterLabels = { "All", "Photo", "Video", "Audio", "Docs", "Recent" };
 63        private readonly string[] SortLabels = { "Name", "Size", "Type", "Date" };
 64
 65        // ===== File Browser =====
 66        private string currentPath; // null = search mode
 67        private readonly List<FolderNode> treeFolders = new List<FolderNode>();
 68        private int treeHover = -1;
 69        private DriveInfo[] drives;
 70        private int driveHover = -1;
 71        private string[] breadcrumbs;
 72        private int breadcrumbHover = -1;
 73        private bool browseMode; // true = folder browser, false = search results
 74
 75        // ===== Width =====
 76        private int baseWidth = CompactW;
 77        private int extraWidth;
 78
 79        // ===== MFT =====
 80        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
 81        private static extern IntPtr CreateFile(string f, uint a, uint s, IntPtr sa, uint d, uint fl, IntPtr t);
 82        [DllImport("kernel32.dll", SetLastError = true)]
 83        [return: MarshalAs(UnmanagedType.Bool)]
 84        private static extern bool DeviceIoControl(IntPtr h, uint c, IntPtr i, int ib, IntPtr o, int ob, out uint r, IntPtr ov);
 85        [DllImport("kernel32.dll")]
 86        private static extern bool CloseHandle(IntPtr h);
 87        [DllImport("gdi32.dll")]
 88        private static extern bool BitBlt(IntPtr hdc, int x, int y, int cx, int cy, IntPtr src, int x1, int y1, uint rop);
 89
 90        private static volatile bool mftReady, mftStarted;
 91        private static string[] mftNames, mftNamesOrig;
 92        private static ulong[] mftParents;
 93        private static char[] mftDrives;
 94        private static int mftCount;
 95        private static readonly Dictionary<ulong, KeyValuePair<string, ulong>> mftDirs = new Dictionary<ulong, KeyValuePair<string, ulong>>();
 96        private static readonly object mftLock = new object();
 97
 98        // ===== Extensions =====
 99        private static readonly HashSet<string> PhotoExts = new HashSet<string>(StringComparer.OrdinalIgnoreCase){".jpg",".jpeg",".png",".bmp",".gif",".tiff",".tif",".webp",".ico",".svg"};
100        private static readonly HashSet<string> VideoExts = new HashSet<string>(StringComparer.OrdinalIgnoreCase){".mp4",".avi",".mkv",".wmv",".mov",".flv",".webm",".m4v",".mpg",".mpeg"};
101        private static readonly HashSet<string> AudioExts = new HashSet<string>(StringComparer.OrdinalIgnoreCase){".mp3",".wav",".flac",".ogg",".m4a",".aac",".opus",".wma"};
102        private static readonly HashSet<string> DocExts = new HashSet<string>(StringComparer.OrdinalIgnoreCase){".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".txt",".rtf",".csv"};
103
104        private class SearchResult { public string FullPath, FileName, Directory, Ext; public long Size; public DateTime Modified; public bool MetaLoaded, IsFolder; public int MftIdx; }
105        private class FolderNode { public string Name, FullPath; public int Depth; public bool Expanded, HasChildren; public List<FolderNode> Children; }
106
107        // ===== MFT scan (same as before) =====
108        public static void BeginMftScan()
109        {
110            if (mftStarted) return; mftStarted = true;
111            new Thread(() => {
112                try {
113                    var names = new List<string>(2000000); var orig = new List<string>(2000000);
114                    var parents = new List<ulong>(2000000); var drvs = new List<char>(2000000);
115                    foreach (var d in DriveInfo.GetDrives()) { if (!d.IsReady || d.DriveFormat != "NTFS") continue; ScanMft(d.Name[0], names, orig, parents, drvs); }
116                    mftNames = names.ToArray(); mftNamesOrig = orig.ToArray(); mftParents = parents.ToArray(); mftDrives = drvs.ToArray(); mftCount = mftNames.Length; mftReady = true;
117                } catch { }
118            }) { IsBackground = true, Name = "MFTBoot" }.Start();
119        }
120
121        private static void ScanMft(char letter, List<string> names, List<string> orig, List<ulong> parents, List<char> drvs)
122        {
123            IntPtr hVol = CreateFile(@"\\.\" + letter + ":", 0x80000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
124            if (hVol == new IntPtr(-1)) return;
125            try {
126                IntPtr jBuf = Marshal.AllocHGlobal(80); uint jr;
127                if (!DeviceIoControl(hVol, 0x000900f4, IntPtr.Zero, 0, jBuf, 80, out jr, IntPtr.Zero)) { Marshal.FreeHGlobal(jBuf); return; }
128                long nextUsn = Marshal.ReadInt64(jBuf, 16); Marshal.FreeHGlobal(jBuf);
129                IntPtr med = Marshal.AllocHGlobal(24); Marshal.WriteInt64(med, 0, 0); Marshal.WriteInt64(med, 8, 0); Marshal.WriteInt64(med, 16, nextUsn);
130                int outSz = 0x10000 + 8; IntPtr outB = Marshal.AllocHGlobal(outSz); uint ret;
131                while (DeviceIoControl(hVol, 0x000900b3, med, 24, outB, outSz, out ret, IntPtr.Zero)) {
132                    if (ret <= 8) break; IntPtr ptr = new IntPtr(outB.ToInt64() + 8); uint rem = ret - 8;
133                    while (rem > 60) { uint rl = (uint)Marshal.ReadInt32(ptr); if (rl < 60 || rl > rem) break;
134                        ulong frn = (ulong)Marshal.ReadInt64(ptr, 8); ulong pfrn = (ulong)Marshal.ReadInt64(ptr, 16);
135                        uint attr = (uint)Marshal.ReadInt32(ptr, 52); int nl = Marshal.ReadInt16(ptr, 56), no = Marshal.ReadInt16(ptr, 58);
136                        if (nl > 0 && no > 0) { string name = Marshal.PtrToStringUni(new IntPtr(ptr.ToInt64() + no), nl / 2);
137                            lock (mftLock) { if ((attr & 0x10) != 0) mftDirs[frn] = new KeyValuePair<string, ulong>(name, pfrn);
138                                else { names.Add(name.ToLowerInvariant()); orig.Add(name); parents.Add(pfrn); drvs.Add(letter); } } }
139                        ptr = new IntPtr(ptr.ToInt64() + rl); rem -= rl; }
140                    Marshal.WriteInt64(med, 0, Marshal.ReadInt64(outB, 0)); }
141                Marshal.FreeHGlobal(med); Marshal.FreeHGlobal(outB);
142            } finally { CloseHandle(hVol); }
143        }
144
145        private static string BuildPath(ulong pfrn, char drv)
146        {
147            var parts = new List<string>(); ulong c = pfrn; int d = 0;
148            lock (mftLock) { while (mftDirs.ContainsKey(c) && d < 64) { var e = mftDirs[c]; parts.Add(e.Key); c = e.Value; d++; } }
149            parts.Reverse(); return drv + ":\\" + string.Join("\\", parts);
150        }
151
152        // ===== Constructor =====
153        public SearchForm()
154        {
155            var screen = Screen.PrimaryScreen.WorkingArea;
156            FormBorderStyle = FormBorderStyle.None; ShowInTaskbar = false; TopMost = true;
157            StartPosition = FormStartPosition.Manual;
158            BackColor = Color.FromArgb(Settings.BlurTintColor.R, Settings.BlurTintColor.G, Settings.BlurTintColor.B);
159            DoubleBuffered = true;
160            SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
161            Width = ExpandedW; Height = screen.Height;
162            Location = new Point(screen.Left - Width, screen.Top);
163            baseWidth = ExpandedW;
164
165            // Search box
166            int sbY = Height - 40;
167            searchBox = new TextBox();
168            searchBox.Font = new Font("Segoe UI", 12f);
169            searchBox.BackColor = Color.FromArgb(Math.Min(255, BackColor.R + 14), Math.Min(255, BackColor.G + 14), Math.Min(255, BackColor.B + 14));
170            searchBox.ForeColor = Color.FromArgb(210, 218, 235);
171            searchBox.BorderStyle = BorderStyle.None;
172            searchBox.Bounds = new Rectangle(16, sbY, Width - 32, 22);
173            searchBox.TextChanged += (s, e) => { debounceTimer.Stop(); debounceTimer.Start(); };
174            searchBox.KeyDown += SearchBox_KeyDown;
175            Controls.Add(searchBox);
176
177            // No render panel — D2D renders directly to Form handle
178            // DWM blur on Form + D2D semi-transparent Clear = glass effect
179            debounceTimer = new System.Windows.Forms.Timer { Interval = 80 };
180            debounceTimer.Tick += (s, e) => { debounceTimer.Stop(); DoSearch(); };
181
182            animTimer = new System.Windows.Forms.Timer { Interval = 16 };
183            animTimer.Tick += AnimTimer_Tick;
184            slideProgress = 0; slideTarget = 1;
185
186            drives = Array.FindAll(DriveInfo.GetDrives(), d => d.IsReady && (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Removable));
187
188            Load += (s, e) => { BlurHelper.Apply(Handle); WinApi.TryEnableRoundedCorners(Handle);
189                NavigateTo(drives.Length > 0 ? drives[0].Name.Substring(0, 2) + "\\" : "C:\\"); };
190            Shown += (s, e) => { searchBox.Focus(); animTimer.Start(); };
191
192            MouseWheel += (s, e) => {
193                int listH = Height - BottomBarH - BreadcrumbH - 8;
194                int maxS = Math.Max(0, results.Count * ItemH - listH);
195                scrollTarget = Math.Max(0, Math.Min(maxS, scrollTarget - e.Delta));
196            };
197            MouseDown += Panel_MouseDown;
198            MouseMove += Panel_MouseMove;
199            MouseDoubleClick += Panel_DoubleClick;
200        }
201
202        // ===== Own D2D context =====
203        private ID2D1SolidColorBrush MakeBrush(uint argb)
204        {
205            float a = ((argb >> 24) & 0xFF) / 255f, r = ((argb >> 16) & 0xFF) / 255f;
206            float g = ((argb >> 8) & 0xFF) / 255f, b = (argb & 0xFF) / 255f;
207            var c = new D2D1_COLOR_F(r, g, b, a);
208            ID2D1SolidColorBrush br;
209            rt.CreateSolidColorBrush(ref c, IntPtr.Zero, out br);
210            return br;
211        }
212
213        private IDWriteTextFormat MakeFormat(string font, float size, bool bold)
214        {
215            IDWriteTextFormat fmt;
216            dwFactory.CreateTextFormat(font, IntPtr.Zero, bold ? 700 : 400, 0, 5, size, "en-us", out fmt);
217            if (fmt != null) { fmt.SetWordWrapping(1); fmt.SetParagraphAlignment(0); }
218            return fmt;
219        }
220
221        private static readonly Guid IID_IDWriteTextFormat = new Guid("9c906818-31d7-4fd3-a151-7c5e225db55a");
222
223        private void D2DDrawText(string text, float x, float y, float w, float h, ID2D1SolidColorBrush brush, IDWriteTextFormat fmt)
224        {
225            if (rt == null || brush == null || fmt == null || string.IsNullOrEmpty(text)) return;
226            if (w <= 0 || h <= 0) return;
227            var rect = new D2D1_RECT_F { left = x, top = y, right = x + w, bottom = y + h };
228            IntPtr pFmt = Marshal.GetComInterfaceForObject(fmt, typeof(IDWriteTextFormat));
229            try { rt.DrawText(text, (uint)text.Length, pFmt, ref rect, (ID2D1Brush)brush, D2D1_DRAW_TEXT_OPTIONS.NONE, 0); }
230            finally { Marshal.Release(pFmt); }
231        }
232
233        private void D2DFillRect(float x, float y, float w, float h, ID2D1SolidColorBrush brush)
234        {
235            if (rt == null || brush == null || w <= 0 || h <= 0) return;
236            var rect = new D2D1_RECT_F { left = x, top = y, right = x + w, bottom = y + h };
237            rt.FillRectangle(ref rect, (ID2D1Brush)brush);
238        }
239
240        private void D2DDrawLine(float x1, float y1, float x2, float y2, ID2D1SolidColorBrush brush, float width)
241        {
242            if (rt == null || brush == null) return;
243            rt.DrawLine(new D2D1_POINT_2F { x = x1, y = y1 }, new D2D1_POINT_2F { x = x2, y = y2 }, (ID2D1Brush)brush, width, null);
244        }
245
246        private void D2DPushClip(float x, float y, float w, float h)
247        {
248            var rect = new D2D1_RECT_F { left = x, top = y, right = x + w, bottom = y + h };
249            rt.PushAxisAlignedClip(ref rect, D2D1_ANTIALIAS_MODE.ALIASED);
250        }
251
252        private static void SLog(string msg)
253        {
254            try { string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WindowCapture");
255                if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
256                File.AppendAllText(Path.Combine(dir, "search_d2d.log"), DateTime.Now.ToString("HH:mm:ss.fff") + "  " + msg + "\r\n");
257            } catch { }
258        }
259
260        private void InitD2D()
261        {
262            try
263            {
264                SLog("InitD2D start, form=" + Width + "x" + Height + " handle=" + Handle);
265
266                D2DApi.D2D1CreateFactory(D2D1_FACTORY_TYPE.SINGLE_THREADED, D2DApi.IID_ID2D1Factory, IntPtr.Zero, out d2dFactory);
267                SLog("D2D factory=" + (d2dFactory != null));
268                if (d2dFactory == null) return;
269
270                var rtProps = new D2D1_RENDER_TARGET_PROPERTIES();
271                var hwndProps = new D2D1_HWND_RENDER_TARGET_PROPERTIES
272                {
273                    hwnd = Handle,
274                    pixelSize = new D2D1_SIZE_U { width = (uint)Width, height = (uint)Height },
275                    presentOptions = D2D1_PRESENT_OPTIONS.NONE
276                };
277                int hr = d2dFactory.CreateHwndRenderTarget(ref rtProps, ref hwndProps, out rt);
278                SLog("CreateHwndRenderTarget hr=0x" + hr.ToString("X8") + " rt=" + (rt != null));
279                if (hr != 0 || rt == null) return;
280
281                object dwObj;
282                D2DApi.DWriteCreateFactory(0, D2DApi.IID_IDWriteFactory, out dwObj);
283                dwFactory = dwObj as IDWriteFactory;
284                SLog("DWrite factory=" + (dwFactory != null));
285                if (dwFactory == null) return;
286
287                // Text formats
288                fmtName = MakeFormat("Segoe UI", 14f, true);
289                fmtPath = MakeFormat("Segoe UI", 9f, false);
290                fmtMeta = MakeFormat("Segoe UI", 9f, false);
291                fmtChip = MakeFormat("Segoe UI", 10f, true);
292                fmtSort = MakeFormat("Segoe UI", 9f, false);
293                fmtTree = MakeFormat("Segoe UI", 10f, false);
294                fmtDrive = MakeFormat("Segoe UI", 10f, true);
295
296                // Brushes
297                brText = MakeBrush(0xFFD2DAEB); brDim = MakeBrush(0xFF646E82);
298                brAccent = MakeBrush(0xFF4682C8); brSelBg = MakeBrush(0x184682C8);
299                brSelBar = MakeBrush(0xFF4682C8); brHover = MakeBrush(0x0CFFFFFF);
300                brBarBg = MakeBrush(0x28000000); brSep = MakeBrush(0x0AFFFFFF);
301                brPhotoB = MakeBrush(0xFF50B450); brVideoB = MakeBrush(0xFFB464C8);
302                brAudioB = MakeBrush(0xFFC8A03C); brDocB = MakeBrush(0xFF4682C8);
303                brPhotoBg = MakeBrush(0x2350B450); brVideoBg = MakeBrush(0x23B464C8);
304                brAudioBg = MakeBrush(0x23C8A03C); brDocBg = MakeBrush(0x234682C8);
305                brTreeBg = MakeBrush(0x10000000); brDriveBg = MakeBrush(0x15FFFFFF);
306                brDriveUsed = MakeBrush(0xFF4682C8); brFilterAct = MakeBrush(0x284682C8);
307                brFilterIn = MakeBrush(0x0CFFFFFF);
308                uint tintArgb = (uint)((Settings.BlurTintAlpha << 24) | (Settings.BlurTintColor.R << 16) | (Settings.BlurTintColor.G << 8) | Settings.BlurTintColor.B);
309                brBg = MakeBrush(tintArgb);
310
311                SLog("InitD2D SUCCESS");
312            }
313            catch (Exception ex) { SLog("InitD2D EXCEPTION: " + ex.Message); }
314        }
315
316        // ===== Navigation =====
317        private void NavigateTo(string path)
318        {
319            currentPath = path; browseMode = true;
320            breadcrumbs = path.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
321            lock (results) results.Clear();
322            scrollOffset = 0; scrollTarget = 0; selectedIndex = 0;
323
324            // Load folder contents
325            new Thread(() => {
326                try {
327                    var items = new List<SearchResult>();
328                    if (Directory.Exists(path)) {
329                        foreach (var d in Directory.EnumerateDirectories(path)) {
330                            try { string n = Path.GetFileName(d); items.Add(new SearchResult { FullPath = d, FileName = n, Directory = path, IsFolder = true, MetaLoaded = true }); } catch { }
331                        }
332                        foreach (var f in Directory.EnumerateFiles(path)) {
333                            try { var fi = new FileInfo(f); string ext = fi.Extension.ToLowerInvariant();
334                                items.Add(new SearchResult { FullPath = f, FileName = fi.Name, Directory = path, Ext = ext, Size = fi.Length, Modified = fi.LastWriteTime, MetaLoaded = true }); } catch { }
335                        }
336                    }
337                    try { BeginInvoke((Action)(() => { lock (results) { results.Clear(); results.AddRange(items); } totalFound = items.Count; statusText = totalFound + " items"; needsRepaint = true; })); } catch { }
338                } catch { }
339            }) { IsBackground = true }.Start();
340            BuildTree(path);
341            needsRepaint = true;
342        }
343
344        private void BuildTree(string rootPath)
345        {
346            string drive = rootPath.Substring(0, 3);
347            treeFolders.Clear();
348            try {
349                foreach (var d in Directory.EnumerateDirectories(drive)) {
350                    try { string n = Path.GetFileName(d); bool hasSub = false;
351                        try { hasSub = Directory.EnumerateDirectories(d).GetEnumerator().MoveNext(); } catch { }
352                        var node = new FolderNode { Name = n, FullPath = d, Depth = 0, HasChildren = hasSub, Children = new List<FolderNode>() };
353                        treeFolders.Add(node);
354                        // Expand current path
355                        if (rootPath.StartsWith(d, StringComparison.OrdinalIgnoreCase)) { node.Expanded = true; ExpandNode(node, rootPath); }
356                    } catch { }
357                }
358            } catch { }
359        }
360
361        private void ExpandNode(FolderNode node, string targetPath)
362        {
363            if (node.Children.Count > 0) return;
364            try {
365                foreach (var d in Directory.EnumerateDirectories(node.FullPath)) {
366                    try { string n = Path.GetFileName(d); bool hasSub = false;
367                        try { hasSub = Directory.EnumerateDirectories(d).GetEnumerator().MoveNext(); } catch { }
368                        var child = new FolderNode { Name = n, FullPath = d, Depth = node.Depth + 1, HasChildren = hasSub, Children = new List<FolderNode>() };
369                        node.Children.Add(child);
370                        if (targetPath != null && targetPath.StartsWith(d, StringComparison.OrdinalIgnoreCase)) { child.Expanded = true; ExpandNode(child, targetPath); }
371                    } catch { }
372                }
373            } catch { }
374        }
375
376        // ===== Slide =====
377        public void SlideIn() { slideTarget = 1; animTimer.Start(); }
378        public void SlideOut() { slideTarget = 0; extraWidth = 0; animTimer.Start(); }
379        public bool IsSlideVisible { get { return slideProgress > 0.01f || slideTarget > 0; } }
380        public void SetExtraWidth(int extra) { extraWidth = Math.Max(0, Math.Min(600, extra)); int nw = baseWidth + extraWidth; if (Width != nw) { Width = nw; searchBox.Width = Width - 32;
381            if (rt != null) { var sz = new D2D1_SIZE_U { width = (uint)Width, height = (uint)Height }; rt.Resize(ref sz); } needsRepaint = true; } }
382
383        private bool needsRepaint = true;
384
385        private void AnimTimer_Tick(object s, EventArgs ev)
386        {
387            bool moved = false;
388            float d = slideTarget - slideProgress;
389            if (Math.Abs(d) > 0.005f) { slideProgress += d * 0.18f; if (Math.Abs(slideTarget - slideProgress) < 0.005f) slideProgress = slideTarget; if (slideTarget <= 0 && slideProgress <= 0) { animTimer.Stop(); Hide(); return; } var scr = Screen.PrimaryScreen.WorkingArea; float e = slideProgress * slideProgress * (3f - 2f * slideProgress); Left = scr.Left + (int)(-Width + Width * e); moved = true; }
390            float sd = scrollTarget - scrollOffset;
391            bool scrolled = false;
392            if (Math.Abs(sd) > 0.5f) { scrollOffset += sd * 0.22f; scrolled = true; } else if (Math.Abs(sd) > 0.01f) { scrollOffset = scrollTarget; scrolled = true; }
393            if (scrolled || needsRepaint || moved) { RequestRepaint(); needsRepaint = false; }
394        }
395
396        new public void Invalidate() { needsRepaint = true; }
397
398        // ===== Search =====
399        private void DoSearch()
400        {
401            string query = searchBox.Text.Trim();
402            if (string.IsNullOrEmpty(query)) { if (currentPath != null) NavigateTo(currentPath); return; }
403            browseMode = false;
404            cancelToken++; int token = cancelToken;
405            lock (results) results.Clear();
406            scrollOffset = 0; scrollTarget = 0; selectedIndex = 0; totalFound = 0;
407            statusText = "Searching..."; needsRepaint = true;
408            if (!mftReady) return;
409            string qLow = query.ToLowerInvariant();
410            FileFilter filt = currentFilter; SortMode sort = currentSort;
411            new Thread(() => {
412                try {
413                    var batch = new List<SearchResult>(5000);
414                    for (int i = 0; i < mftCount && batch.Count < 5000; i++) {
415                        if (token != cancelToken) return;
416                        if (!mftNames[i].Contains(qLow)) continue;
417                        int dot = mftNames[i].LastIndexOf('.'); string ext = dot >= 0 ? mftNames[i].Substring(dot) : "";
418                        if (!MatchFilter(ext, filt)) continue;
419                        batch.Add(new SearchResult { FileName = mftNamesOrig[i], Ext = ext, MftIdx = i });
420                        if (batch.Count == 50) { var snap = batch.ToArray(); int tf = batch.Count; try { BeginInvoke((Action)(() => { if (token != cancelToken) return; lock (results) { results.Clear(); results.AddRange(snap); } totalFound = tf; statusText = tf + " found..."; needsRepaint = true; })); } catch { } }
421                    }
422                    if (sort == SortMode.Name) batch.Sort((a, b) => string.Compare(a.FileName, b.FileName, StringComparison.OrdinalIgnoreCase));
423                    else if (sort == SortMode.Type) batch.Sort((a, b) => string.Compare(a.Ext, b.Ext, StringComparison.OrdinalIgnoreCase));
424                    var fin = batch.ToArray(); int fc = batch.Count;
425                    try { BeginInvoke((Action)(() => { if (token != cancelToken) return; lock (results) { results.Clear(); results.AddRange(fin); } totalFound = fc; statusText = fc.ToString("N0") + " files"; needsRepaint = true;
426                        new Thread(() => LoadMeta(token)) { IsBackground = true }.Start(); })); } catch { }
427                } catch { }
428            }) { IsBackground = true }.Start();
429        }
430
431        private static bool MatchFilter(string ext, FileFilter f)
432        { switch (f) { case FileFilter.Photo: return PhotoExts.Contains(ext); case FileFilter.Video: return VideoExts.Contains(ext); case FileFilter.Audio: return AudioExts.Contains(ext); case FileFilter.Docs: return DocExts.Contains(ext); default: return true; } }
433
434        private void LoadMeta(int token)
435        { int cnt; lock (results) cnt = results.Count;
436            for (int i = 0; i < cnt; i++) { if (token != cancelToken) return; SearchResult r; lock (results) { if (i >= results.Count) break; r = results[i]; }
437                if (r.MetaLoaded) continue; r.Directory = BuildPath(mftParents[r.MftIdx], mftDrives[r.MftIdx]); r.FullPath = r.Directory + "\\" + r.FileName;
438                try { var fi = new FileInfo(r.FullPath); if (fi.Exists) { r.Size = fi.Length; r.Modified = fi.LastWriteTime; } } catch { } r.MetaLoaded = true;
439                if (i == 30 || i == 100 || i % 500 == 0) try { BeginInvoke((Action)(() => { if (token == cancelToken) needsRepaint = true; })); } catch { } }
440            try { BeginInvoke((Action)(() => { if (token == cancelToken) { if (currentSort == SortMode.Date) lock (results) results.Sort((a, b) => b.Modified.CompareTo(a.Modified));
441                else if (currentSort == SortMode.Size) lock (results) results.Sort((a, b) => b.Size.CompareTo(a.Size)); needsRepaint = true; } })); } catch { } }
442
443        private void SearchBox_KeyDown(object s, KeyEventArgs e)
444        { if (e.KeyCode == Keys.Escape) { SlideOut(); if (PanelHidden != null) PanelHidden(); e.Handled = true; }
445            else if (e.KeyCode == Keys.Down) { lock (results) { if (selectedIndex < results.Count - 1) selectedIndex++; } EnsureVisible(); needsRepaint = true; e.Handled = true; }
446            else if (e.KeyCode == Keys.Up) { if (selectedIndex > 0) selectedIndex--; EnsureVisible(); needsRepaint = true; e.Handled = true; }
447            else if (e.KeyCode == Keys.Enter) { OpenResult(selectedIndex); e.Handled = true; }
448            else if (e.KeyCode == Keys.Back && searchBox.Text.Length == 0 && browseMode && currentPath != null) { string parent = Path.GetDirectoryName(currentPath); if (parent != null) NavigateTo(parent); e.Handled = true; } }
449
450        private void EnsureVisible() { int lh = Height - BottomBarH - BreadcrumbH - 8; float it = selectedIndex * ItemH - scrollTarget; if (it < 0) scrollTarget += it; else if (it + ItemH > lh) scrollTarget += (it + ItemH - lh); if (scrollTarget < 0) scrollTarget = 0; }
451
452        private void OpenResult(int idx) {
453            SearchResult r; lock (results) { r = idx >= 0 && idx < results.Count ? results[idx] : null; }
454            if (r == null) return;
455            if (!r.MetaLoaded && r.MftIdx >= 0) { r.Directory = BuildPath(mftParents[r.MftIdx], mftDrives[r.MftIdx]); r.FullPath = r.Directory + "\\" + r.FileName; r.MetaLoaded = true; }
456            if (r.IsFolder) { NavigateTo(r.FullPath); return; }
457            try { string ext = r.Ext ?? ""; if (MediaTypes.IsImage(ext) || MediaTypes.IsVideo(ext) || MediaTypes.IsAudio(ext)) new EditorForm(r.FullPath).Show(); else System.Diagnostics.Process.Start(r.FullPath); SlideOut(); } catch { } }
458
459        private string FormatSize(long b) { if (b <= 0) return ""; if (b < 1024) return b + " B"; if (b < 1048576) return (b / 1024) + " KB"; if (b < 1073741824L) return (b / 1048576) + " MB"; return (b / 1073741824.0).ToString("F1") + " GB"; }
460
461        // ===== Hybrid Rendering: DWM blur + GDI TextRenderer (fast) =====
462        [DllImport("gdi32.dll")]
463        private static extern IntPtr CreateDIBSection(IntPtr hdc, ref BITMAPINFO2 bmi, int u, out IntPtr bits, IntPtr sec, int off);
464        [StructLayout(LayoutKind.Sequential)]
465        private struct BITMAPINFO2 { public int biSize, biWidth, biHeight; public short biPlanes, biBitCount; public int biCompression, biSizeImage, biXPelsPerMeter, biYPelsPerMeter, biClrUsed, biClrImportant; }
466
467        private Bitmap offBmp;
468        private IntPtr offDc, offDib, offOldBmp;
469        private int offW, offH;
470        private Font gdiName, gdiPath, gdiMeta, gdiChip, gdiSort, gdiTree, gdiDrive;
471        // (removed unused TextFormatFlags TFF — was assigned but never read)
472
473        private void EnsureGdiFonts()
474        {
475            if (gdiName != null) return;
476            gdiName = new Font("Segoe UI Semibold", 10.5f);
477            gdiPath = new Font("Segoe UI", 8.5f);
478            gdiMeta = new Font("Segoe UI", 8f);
479            gdiChip = new Font("Segoe UI Semibold", 9f);
480            gdiSort = new Font("Segoe UI", 8f);
481            gdiTree = new Font("Segoe UI", 9f);
482            gdiDrive = new Font("Segoe UI Semibold", 9f);
483        }
484
485        private void EnsureOffBmp(int w, int h)
486        {
487            if (offDc != IntPtr.Zero && offW == w && offH == h) return;
488            if (offBmp != null) { offBmp.Dispose(); offBmp = null; }
489            if (offDc != IntPtr.Zero && offOldBmp != IntPtr.Zero) WinApi.SelectObject(offDc, offOldBmp);
490            if (offDib != IntPtr.Zero) WinApi.DeleteObject(offDib);
491            if (offDc != IntPtr.Zero) WinApi.DeleteDC(offDc);
492            IntPtr scr = WinApi.GetDC(IntPtr.Zero);
493            offDc = WinApi.CreateCompatibleDC(scr);
494            var bmi = new BITMAPINFO2 { biSize = 40, biWidth = w, biHeight = -h, biPlanes = 1, biBitCount = 32 };
495            IntPtr bits; offDib = CreateDIBSection(offDc, ref bmi, 0, out bits, IntPtr.Zero, 0);
496            offOldBmp = WinApi.SelectObject(offDc, offDib);
497            WinApi.ReleaseDC(IntPtr.Zero, scr);
498            offBmp = new Bitmap(w, h, w * 4, PixelFormat.Format32bppPArgb, bits);
499            offW = w; offH = h;
500        }
501
502        private static Bitmap noiseTexture;
503        private static int noiseCachedIntensity = -1;
504        private static Bitmap GetNoiseTexture(int i) { if (i <= 0) return null; if (noiseTexture != null && noiseCachedIntensity == i) return noiseTexture; if (noiseTexture != null) noiseTexture.Dispose(); const int sz = 128; var bmp = new Bitmap(sz, sz, PixelFormat.Format32bppArgb); var rng = new Random(42); int aMin = Math.Max(1, i / 2), aMax = Math.Max(aMin + 1, i); var bits = bmp.LockBits(new Rectangle(0, 0, sz, sz), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); unsafe { byte* p = (byte*)bits.Scan0; for (int j = 0; j < sz * sz; j++) { byte v = (byte)rng.Next(160, 255); byte a = (byte)rng.Next(aMin, aMax + 1); p[0]=v;p[1]=v;p[2]=v;p[3]=a;p+=4; } } bmp.UnlockBits(bits); noiseTexture = bmp; noiseCachedIntensity = i; return bmp; }
505
506        // Cached brushes/pens for zero-alloc rendering
507        private SolidBrush cbrBarBg, cbrTreeBg, cbrSelBg, cbrHover, cbrSep, cbrAccentBg, cbrFilterIn, cbrDriveBar;
508        private SolidBrush cbrAccent30, cbrAccentSolid, cbrBreadcrumbBg;
509        private Pen cpenEdge, cpenSep, cpenTreeSep;
510
511        private void EnsureCachedBrushes()
512        {
513            if (cbrBarBg != null) return;
514            Color ac = Color.FromArgb(70, 130, 200);
515            cbrBarBg = new SolidBrush(Color.FromArgb(40, 0, 0, 0));
516            cbrTreeBg = new SolidBrush(Color.FromArgb(16, 0, 0, 0));
517            cbrSelBg = new SolidBrush(Color.FromArgb(24, ac));
518            cbrHover = new SolidBrush(Color.FromArgb(12, 255, 255, 255));
519            cbrSep = new SolidBrush(Color.FromArgb(10, 255, 255, 255));
520            cbrAccentBg = new SolidBrush(Color.FromArgb(40, ac));
521            cbrFilterIn = new SolidBrush(Color.FromArgb(12, 255, 255, 255));
522            cbrDriveBar = new SolidBrush(Color.FromArgb(20, 255, 255, 255));
523            cbrAccent30 = new SolidBrush(Color.FromArgb(30, ac));
524            cbrAccentSolid = new SolidBrush(ac);
525            cbrBreadcrumbBg = new SolidBrush(Color.FromArgb(30, 0, 0, 0));
526            cpenEdge = new Pen(Color.FromArgb(25, ac));
527            cpenSep = new Pen(Color.FromArgb(10, 255, 255, 255));
528            cpenTreeSep = new Pen(Color.FromArgb(15, 255, 255, 255));
529        }
530
531        // Cached noise bitmap blitted once into tint background
532        private Bitmap cachedBg;
533        private int cachedBgW, cachedBgH;
534
535        private void EnsureCachedBg(int w, int h)
536        {
537            if (cachedBg != null && cachedBgW == w && cachedBgH == h) return;
538            if (cachedBg != null) cachedBg.Dispose();
539            cachedBg = new Bitmap(w, h, PixelFormat.Format32bppPArgb);
540            using (var g = Graphics.FromImage(cachedBg))
541            {
542                // Tint (same as viewer OnPaintBackground)
543                g.Clear(Color.FromArgb(Settings.BlurTintAlpha, Settings.BlurTintColor));
544                // Noise grain on top
545                int ni = Settings.NoiseIntensity;
546                if (ni > 0) { var ns = GetNoiseTexture(ni); if (ns != null) using (var tb = new TextureBrush(ns, WrapMode.Tile)) g.FillRectangle(tb, 0, 0, w, h); }
547            }
548            cachedBgW = w; cachedBgH = h;
549        }
550
551        private void RequestRepaint() { base.Invalidate(); }
552
553        // Cached GDI+ brushes for DrawString
554        private SolidBrush gbrText, gbrDim, gbrAccent;
555
556        private void EnsureGdiBrushes()
557        {
558            if (gbrText != null) return;
559            gbrText = new SolidBrush(Color.FromArgb(210, 218, 235));
560            gbrDim = new SolidBrush(Color.FromArgb(100, 110, 130));
561            gbrAccent = new SolidBrush(Color.FromArgb(70, 130, 200));
562        }
563
564        private void RenderContent(Graphics g, int w, int h)
565        {
566            // legacy — unused now
567        }
568
569        private void RenderContentFast(Graphics g, int w, int h)
570        {
571            EnsureGdiBrushes();
572            g.DrawLine(cpenEdge, w - 1, 0, w - 1, h);
573
574            // Left panel
575            int listX = TreeW, listW = w - TreeW, treeH = h - BottomBarH;
576            g.FillRectangle(cbrTreeBg, 0, 0, TreeW, treeH);
577            if (treeH > 0) {
578                g.SetClip(new Rectangle(0, 0, TreeW, treeH));
579                int ty = 4;
580                for (int i = 0; i < drives.Length; i++) {
581                    var dr = drives[i]; string label = dr.Name.Substring(0, 2);
582                    bool dact = currentPath != null && currentPath.StartsWith(label, StringComparison.OrdinalIgnoreCase);
583                    if (dact) g.FillRectangle(cbrAccent30, 0, ty, TreeW, 24);
584                    else if (driveHover == i) g.FillRectangle(cbrHover, 0, ty, TreeW, 24);
585                    string dl = label + "\\";
586                    try { dl += "  " + FormatSize(dr.TotalSize - dr.AvailableFreeSpace) + "/" + FormatSize(dr.TotalSize); } catch { }
587                    g.DrawString(dl, gdiDrive, dact ? gbrAccent : gbrText, 8, ty + 3);
588                    try { float u = 1f - (float)((double)dr.AvailableFreeSpace / dr.TotalSize); g.FillRectangle(cbrDriveBar, 8, ty + 21, TreeW - 16, 2); g.FillRectangle(cbrAccentSolid, 8, ty + 21, (int)((TreeW - 16) * u), 2); } catch { }
589                    ty += 26;
590                }
591                g.DrawLine(cpenTreeSep, 4, ty + 2, TreeW - 4, ty + 2); ty += 6;
592                var flatTree = new List<FolderNode>(); FlattenTree(treeFolders, flatTree);
593                for (int i = 0; i < flatTree.Count; i++) {
594                    var nd = flatTree[i]; int iy = ty + i * 22 - 0;
595                    if (iy + 22 < 0 || iy > treeH) continue;
596                    bool tact = currentPath != null && nd.FullPath.Equals(currentPath.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase);
597                    if (tact) g.FillRectangle(cbrSelBg, 0, iy, TreeW, 22);
598                    else if (treeHover == i) g.FillRectangle(cbrHover, 0, iy, TreeW, 22);
599                    string pfx = nd.HasChildren ? (nd.Expanded ? "\u25BE " : "\u25B8 ") : "   ";
600                    g.DrawString(pfx + nd.Name, gdiTree, tact ? gbrAccent : gbrText, 8 + nd.Depth * 14, iy + 2);
601                }
602                g.ResetClip();
603            }
604            g.DrawLine(cpenTreeSep, TreeW, 0, TreeW, h - BottomBarH);
605
606            // Breadcrumb
607            g.FillRectangle(cbrBreadcrumbBg, listX, 0, listW, BreadcrumbH);
608            float bx = listX + 8;
609            if (breadcrumbs != null) for (int i = 0; i < breadcrumbs.Length; i++) {
610                g.DrawString(breadcrumbs[i], gdiChip, (breadcrumbHover == i) ? gbrAccent : gbrText, bx, 5);
611                bx += breadcrumbs[i].Length * 8 + 4;
612                if (i < breadcrumbs.Length - 1) { g.DrawString("\u203A", gdiSort, gbrDim, bx, 5); bx += 12; }
613            }
614
615            // File list
616            int fileY = BreadcrumbH, fileH = h - fileY - BottomBarH;
617            if (fileH > 0 && listW > 0) {
618                g.SetClip(new Rectangle(listX, fileY, listW, fileH));
619                int count; lock (results) count = results.Count;
620                int iS = (int)scrollOffset;
621                int first = Math.Max(0, iS / ItemH), last = Math.Min(count - 1, (iS + fileH) / ItemH + 1);
622                long mx = 0; lock (results) { for (int j = first; j <= last && j < results.Count; j++) if (results[j].Size > mx) mx = results[j].Size; }
623                for (int i = first; i <= last; i++) {
624                    int iy = fileY + i * ItemH - iS;
625                    SearchResult item; lock (results) { if (i >= results.Count) break; item = results[i]; }
626                    bool sel = (i == selectedIndex), hov = (i == hoverIndex);
627                    int ix = listX + 8, iw = listW - 16;
628                    if (sel) { g.FillRectangle(cbrSelBg, listX, iy, listW, ItemH); g.FillRectangle(cbrAccentSolid, listX, iy, 3, ItemH); }
629                    else if (hov) g.FillRectangle(cbrHover, listX, iy, listW, ItemH);
630                    float nx = ix;
631                    if (item.IsFolder) {
632                        g.DrawString("\uD83D\uDCC1", gdiName, gbrAccent, ix, iy + 2);
633                        g.DrawString(item.FileName, gdiName, sel ? gbrAccent : gbrText, ix + 22, iy + 4);
634                    } else {
635                        if (!string.IsNullOrEmpty(item.Ext)) {
636                            Color bc2 = PhotoExts.Contains(item.Ext) ? Color.FromArgb(80,180,80) : VideoExts.Contains(item.Ext) ? Color.FromArgb(180,100,200) : AudioExts.Contains(item.Ext) ? Color.FromArgb(200,160,60) : Color.FromArgb(70,130,200);
637                            int ew = item.Ext.Length * 7 + 8;
638                            using (var bb = new SolidBrush(Color.FromArgb(35, bc2))) g.FillRectangle(bb, ix, iy + 5, ew, 14);
639                            using (var bt = new SolidBrush(bc2)) g.DrawString(item.Ext, gdiMeta, bt, ix + 4, iy + 5);
640                            nx = ix + ew + 6;
641                        }
642                        g.DrawString(item.FileName, gdiName, sel ? gbrAccent : gbrText, nx, iy + 2);
643                    }
644                    if (item.Directory != null) g.DrawString(item.Directory, gdiPath, gbrDim, ix, iy + 24);
645                    string meta = FormatSize(item.Size); if (item.Modified > DateTime.MinValue) meta += "  " + item.Modified.ToString("dd.MM.yy");
646                    if (meta.Length > 0) g.DrawString(meta, gdiMeta, gbrDim, listX + listW - meta.Length * 7 - 14, iy + 4);
647                    if (item.Size > 0 && mx > 0) { float ratio = (float)((double)item.Size / mx); g.FillRectangle(cbrAccent30, listX + listW - (int)(iw * ratio) - 8, iy + ItemH - 3, (int)(iw * ratio), 2); }
648                    g.DrawLine(cpenSep, ix, iy + ItemH - 1, listX + listW - 8, iy + ItemH - 1);
649                }
650                g.ResetClip();
651            }
652
653            // Bottom bar
654            int barY = h - BottomBarH;
655            g.FillRectangle(cbrBarBg, 0, barY, w, BottomBarH);
656            float cx = 14;
657            for (int i = 0; i < SortLabels.Length; i++) { bool act = ((int)currentSort == i); string lbl = (act && sortDescending ? "\u25BC " : act ? "\u25B2 " : "") + SortLabels[i]; g.DrawString(lbl, gdiSort, act ? gbrText : gbrDim, cx, barY + 4); cx += lbl.Length * 7 + 10; }
658            cx = 14;
659            for (int i = 0; i < FilterLabels.Length; i++) { bool act = ((int)currentFilter == i); int cw = FilterLabels[i].Length * 8 + 16;
660                g.FillRectangle(act ? cbrAccentBg : cbrFilterIn, cx, barY + 22, cw, 20);
661                g.DrawString(FilterLabels[i], gdiChip, act ? gbrAccent : gbrDim, cx + 8, barY + 23); cx += cw + 4; }
662            g.DrawLine(searchBox.Focused ? cpenEdge : cpenSep, 16, barY + 46, w - 16, barY + 46);
663            g.DrawString(statusText, gdiSort, gbrDim, 16, h - 18);
664        }
665
666        private void GetBadgeBrushes(string ext, out ID2D1SolidColorBrush text, out ID2D1SolidColorBrush bg)
667        { if (PhotoExts.Contains(ext)) { text = brPhotoB; bg = brPhotoBg; } else if (VideoExts.Contains(ext)) { text = brVideoB; bg = brVideoBg; }
668            else if (AudioExts.Contains(ext)) { text = brAudioB; bg = brAudioBg; } else if (DocExts.Contains(ext)) { text = brDocB; bg = brDocBg; }
669            else { text = brAccent; bg = brFilterIn; } }
670
671        private void FlattenTree(List<FolderNode> nodes, List<FolderNode> flat)
672        { foreach (var n in nodes) { flat.Add(n); if (n.Expanded && n.Children != null) FlattenTree(n.Children, flat); } }
673
674        // ===== Mouse =====
675        private int GetDriveAreaH() { return 4 + drives.Length * 26 + 6; }
676        private int GetTreeStartY() { return GetDriveAreaH(); }
677
678        private void Panel_MouseDown(object s, MouseEventArgs e)
679        {
680            if (e.Button != MouseButtons.Left) return;
681            int barY = Height - BottomBarH;
682            int driveH = GetDriveAreaH();
683
684            // Left panel: drives + tree
685            if (e.X < TreeW && e.Y < barY) {
686                // Drives
687                if (e.Y < driveH) {
688                    int idx = (e.Y - 4) / 26;
689                    if (idx >= 0 && idx < drives.Length) { NavigateTo(drives[idx].Name.Substring(0, 2) + "\\"); return; }
690                }
691                // Tree folders
692                int treeStartY = GetTreeStartY();
693                var flat = new List<FolderNode>(); FlattenTree(treeFolders, flat);
694                int tidx = (e.Y - treeStartY + 0) / 22;
695                if (tidx >= 0 && tidx < flat.Count) {
696                    var node = flat[tidx];
697                    if (node.HasChildren) { node.Expanded = !node.Expanded; if (node.Expanded && node.Children.Count == 0) ExpandNode(node, null); }
698                    NavigateTo(node.FullPath);
699                }
700                return;
701            }
702
703            // Sort
704            if (e.Y >= barY + 2 && e.Y < barY + 20) { int cx2 = 14; for (int i = 0; i < SortLabels.Length; i++) { int cw = SortLabels[i].Length * 7 + 18; if (e.X >= cx2 && e.X < cx2 + cw) { if ((int)currentSort == i) sortDescending = !sortDescending; else { currentSort = (SortMode)i; sortDescending = (i >= 2); } DoSearch(); return; } cx2 += cw; } }
705
706            // Filters
707            if (e.Y >= barY + 20 && e.Y < barY + 46) { int cx2 = 14; for (int i = 0; i < FilterLabels.Length; i++) { int cw = FilterLabels[i].Length * 8 + 16; if (e.X >= cx2 && e.X < cx2 + cw) { currentFilter = (FileFilter)i; DoSearch(); return; } cx2 += cw + 4; } }
708
709            // File list
710            int fileY = BreadcrumbH, fileH = barY - fileY;
711            if (e.X >= TreeW && e.Y >= fileY && e.Y < fileY + fileH) {
712                int idx = (int)(e.Y - fileY + scrollOffset) / ItemH;
713                lock (results) { if (idx >= 0 && idx < results.Count) { selectedIndex = idx; OpenResult(idx); } }
714            }
715        }
716
717        private void Panel_MouseMove(object s, MouseEventArgs e)
718        {
719            int barY = Height - BottomBarH;
720            int fileY = BreadcrumbH, fileH = barY - fileY;
721
722            // File list hover
723            if (e.X >= TreeW && e.Y >= fileY && e.Y < fileY + fileH) {
724                int idx = (int)(e.Y - fileY + scrollOffset) / ItemH;
725                if (idx != hoverIndex) { lock (results) hoverIndex = idx < results.Count ? idx : -1; needsRepaint = true; }
726            } else if (hoverIndex >= 0) { hoverIndex = -1; needsRepaint = true; }
727
728            // Drive hover
729            int driveH = GetDriveAreaH();
730            if (e.X < TreeW && e.Y < driveH) {
731                int dh = (e.Y - 4) / 26; if (dh < 0 || dh >= drives.Length) dh = -1;
732                if (dh != driveHover) { driveHover = dh; needsRepaint = true; }
733            } else if (driveHover >= 0) { driveHover = -1; needsRepaint = true; }
734
735            // Tree hover
736            if (e.X < TreeW && e.Y >= driveH && e.Y < barY) {
737                int idx = (e.Y - GetTreeStartY() + 0) / 22;
738                if (idx != treeHover) { treeHover = idx; needsRepaint = true; }
739            } else if (treeHover >= 0) { treeHover = -1; needsRepaint = true; }
740        }
741
742        private void Panel_DoubleClick(object s, MouseEventArgs e)
743        {
744            if (e.Button != MouseButtons.Left) return;
745            int fileY = BreadcrumbH, barY = Height - BottomBarH;
746            if (e.X >= TreeW && e.Y >= fileY && e.Y < barY) {
747                int idx = (int)(e.Y - fileY + scrollOffset) / ItemH;
748                lock (results) { if (idx >= 0 && idx < results.Count && results[idx].IsFolder) NavigateTo(results[idx].FullPath); }
749            }
750        }
751
752        protected override void OnPaintBackground(PaintEventArgs e) { /* handled in OnPaint */ }
753        protected override void OnPaint(PaintEventArgs e)
754        {
755            EnsureGdiFonts();
756            EnsureCachedBrushes();
757            var g = e.Graphics;
758            g.SmoothingMode = SmoothingMode.HighSpeed;
759            g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
760            g.CompositingQuality = CompositingQuality.HighSpeed;
761            int w = Width, h = Height;
762
763            // Background tint
764            g.Clear(Color.FromArgb(Settings.BlurTintAlpha, Settings.BlurTintColor));
765
766            // Noise grain (skip during scroll for speed)
767            bool scrolling = Math.Abs(scrollTarget - scrollOffset) > 1f;
768            if (!scrolling) {
769                int ni = Settings.NoiseIntensity;
770                if (ni > 0) { var ns = GetNoiseTexture(ni); if (ns != null) using (var tb = new TextureBrush(ns, WrapMode.Tile)) g.FillRectangle(tb, 0, 0, w, h); }
771            }
772
773            RenderContentFast(g, w, h);
774        }
775
776        protected override void OnDeactivate(EventArgs e) { base.OnDeactivate(e); }
777        protected override CreateParams CreateParams { get { var cp = base.CreateParams; cp.Style |= unchecked((int)0x80000000); return cp; } }
778
779        protected override void Dispose(bool disposing)
780        {
781            if (disposing) { cancelToken++;
782                // Release D2D resources
783                if (rt != null) { try { Marshal.ReleaseComObject(rt); } catch { } rt = null; }
784                if (d2dFactory != null) { try { Marshal.ReleaseComObject(d2dFactory); } catch { } d2dFactory = null; }
785                if (dwFactory != null) { try { Marshal.ReleaseComObject(dwFactory); } catch { } dwFactory = null; }
786                if (animTimer != null) animTimer.Dispose(); if (debounceTimer != null) debounceTimer.Dispose(); }
787            base.Dispose(disposing);
788        }
789    }
790}