<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: ThePainter</title>
    <description>The latest articles on Forem by ThePainter (@saidfatah).</description>
    <link>https://forem.com/saidfatah</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1137820%2F8200e883-3a2c-4158-b112-559cf393b07b.jpeg</url>
      <title>Forem: ThePainter</title>
      <link>https://forem.com/saidfatah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/saidfatah"/>
    <language>en</language>
    <item>
      <title>How I Made an Electron Window Draggable and Kept Mouse Enter/Leave Detection</title>
      <dc:creator>ThePainter</dc:creator>
      <pubDate>Wed, 03 Dec 2025 12:41:33 +0000</pubDate>
      <link>https://forem.com/saidfatah/how-i-made-an-electron-window-draggable-and-kept-mouse-enterleave-detection-1c0h</link>
      <guid>https://forem.com/saidfatah/how-i-made-an-electron-window-draggable-and-kept-mouse-enterleave-detection-1c0h</guid>
      <description>&lt;p&gt;(this article requires you have some good understanding of how electron works) &lt;/p&gt;

&lt;p&gt;Sad story: I wanted my app to be fully draggable and still change its height and width on mouse enter and mouse leave. But you can’t have both out of the box—making a window draggable requires ignoring mouse events in the drag area, and I want almost the entire window to act as the drag space. This means normal enter/leave detection breaks immediately.&lt;/p&gt;

&lt;p&gt;when mouse is not inside : &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jwtqfootbmnn174debq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jwtqfootbmnn174debq.png" alt="Focus app widget mouse outside"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;when mouse is inside : &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq6z2avs8f4ziuuadfuoh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq6z2avs8f4ziuuadfuoh.png" alt="Focus app widget mouse inside"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is how I configured the window to look sleek like that :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const mainWindow = new BrowserWindow({
    // the usual width , height ,x , y .. etc
    titleBarStyle: 'hidden',
    backgroundMaterial:"acrylic",
    visualEffectState:"active",
    vibrancy: "popover",    // on MacOS
    frame: false, // make it frame less 
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and to make it dragable you just add this class to the areas you want to make dragAble:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.dragAble {
    -webkit-user-select: none;
    -webkit-app-region: drag;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for the element that you still want to keep intractable , add this class :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.no-drag {
    -webkit-app-region: no-drag;
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;*&lt;em&gt;Alright now lets see how we got to the happy ending : *&lt;/em&gt;&lt;br&gt;
how did I handle mouseEnter and mouseLeave while still making window dragAble ? &lt;/p&gt;

&lt;p&gt;Electron doesn’t provide native mouse-enter or mouse-leave events, so I built them myself using two steps:&lt;/p&gt;

&lt;p&gt;A polling listener: A utility function runs on an interval, checks the cursor position against the window bounds, and determines whether the mouse is inside or outside. It then calls the provided callback with inside.&lt;/p&gt;

&lt;p&gt;Handling the result: When using this listener, we pass a callback that reacts to the inside/outside state and triggers our custom enter/leave logic.&lt;/p&gt;

&lt;p&gt;Here’s the small utility that acts as the “mouse listener”:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function listenToMouseMovement(callback) {
  const id = setInterval(() =&amp;gt; {
    const cursor = screen.getCursorScreenPoint();
    const bounds = esm.mainWindow.getBounds();

    const inside =
      cursor.x &amp;gt;= bounds.x &amp;amp;&amp;amp;
      cursor.x &amp;lt;= bounds.x + bounds.width &amp;amp;&amp;amp;
      cursor.y &amp;gt;= bounds.y &amp;amp;&amp;amp;
      cursor.y &amp;lt;= bounds.y + bounds.height;

    callback({
      inside,
      position: cursor,
    });
  }, 8); // ~60fps polling
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We listen to mouse movement and pass a callback. Electron can fire “inside” and “outside” states very quickly when the cursor sits near the window’s edge, which normally causes a flicker. In my case it’s even worse because the app changes its dimensions on mouse enter and leave, so those rapid state switches create constant resizing flickers.&lt;/p&gt;

&lt;p&gt;To fix this, we debounce both events:&lt;/p&gt;

&lt;p&gt;When the mouse goes inside, we cancel any pending “leave” timeout and delay the “enter” event just enough to confirm the cursor really came back in.&lt;/p&gt;

&lt;p&gt;When the mouse goes outside, we cancel any pending “enter” timeout and delay the “leave” event to confirm it’s truly outside and not immediately returning.&lt;/p&gt;

&lt;p&gt;This small delay prevents false triggers and stops the UI from resizing back and forth near the window edge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;listenToMouseMovement(({ inside }) =&amp;gt; {
  if (esm.isMovingWindow) return; // more on this below

  if (inside) {
     // cancel any pending leave event
     if (leaveTimeout) {
       clearTimeout(leaveTimeout);
       leaveTimeout = null;
     }

     if (esm.mouseWasOutsideWindow) {
        // debounce the enter event
        if (!enterTimeout) {
          enterTimeout = setTimeout(() =&amp;gt; {
            esm.mouseWasOutsideWindow = false;
            // ask renderer to show mouse outside UI 
            mainWindow.webContents.send("onMouseIsInsideTheApp");
          }
        }
     }
     else {
       // cancel any pending enter event
       if (enterTimeout) {
         clearTimeout(enterTimeout);
         enterTimeout = null;
       }

       if (!esm.mouseWasOutsideWindow) {
         // debounce the leave event
         if (!leaveTimeout) {
           leaveTimeout = setTimeout(() =&amp;gt; {
             esm.mouseWasOutsideWindow = true;
             mainWindow.webContents.send("onMouseIsOutsideTheApp");
           }, 120); 
         }
      }
   }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mouse listener alone isn’t enough, because we don’t want to trigger enter/leave checks while the window is being dragged. To handle that, we listen to Electron’s native "will-move", "move", and "moved" events and track a small state flag. When the window starts moving, we set &lt;code&gt;isMovingWindow = true&lt;/code&gt; ( we used this in &lt;code&gt;listenToMouseMovement&lt;/code&gt; ), and once movement stops, we wait briefly before setting it back to false to avoid false detections.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;esm.mainWindow.on("will-move", () =&amp;gt; {
  esm.isMovingWindow = true;
});

esm.mainWindow.on("move", () =&amp;gt; {
  esm.isMovingWindow = true;
  if (esm.moveEndTimeout) {
    clearTimeout(esm.moveEndTimeout);
    esm.moveEndTimeout = null;
  }
});

esm.mainWindow.on("moved", () =&amp;gt; {
  if (esm.moveEndTimeout) clearTimeout(esm.moveEndTimeout);
  esm.moveEndTimeout = setTimeout(() =&amp;gt; {
    esm.isMovingWindow = false;
    esm.moveEndTimeout = null;
  }, 200);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you can check the result in this tweet : &lt;/p&gt;

&lt;p&gt;

&lt;iframe class="tweet-embed" id="tweet-1995877106740592742-550" src="https://platform.twitter.com/embed/Tweet.html?id=1995877106740592742"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1995877106740592742-550');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1995877106740592742&amp;amp;theme=dark"
  }





&lt;/p&gt;

&lt;p&gt;Thanks for reading , this is my first ever article and there is more to come .&lt;/p&gt;

</description>
      <category>electron</category>
      <category>javascript</category>
      <category>ux</category>
    </item>
  </channel>
</rss>
