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}