From cfd21cc71fc8999b216fba53b7c48120657671b1 Mon Sep 17 00:00:00 2001 From: Rajat Patra Date: Fri, 18 Sep 2026 22:57:14 +0530 Subject: [PATCH 1/4] Place the notch inside the work area, so the taskbar cannot cover it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pill was pinned to the monitor's physical edge, so a notch on the edge the taskbar is docked to sat behind it: both windows are topmost, and a notch that is deliberately never focusable cannot win an order decided by activation. #241 already worked out the work area, but spent it on the hover card alone — `work_insets` moved the card and left the pill where it was. `edge_origin` now measures against the work area itself, so the pill clears the taskbar on whichever edge it is docked to, and a screen with no taskbar on that edge is unchanged. `Screen::area()` falls back to the whole monitor when the platform reports nothing usable, rather than pinning the notch to (0, 0) with no span to slide along. The drag's saved ratio moves to the same span. `place_notch` reads it back against the work area, so measuring it against the monitor made every drop next to the taskbar land short of the pointer. The Mac places against `frame` rather than `visibleFrame` on purpose, but what it overlaps there is the menu bar, which macOS lets a notch cover. The taskbar wins the z-order among topmost windows and is a click target of its own, so it is room lost rather than room shared. Not covered: nothing watches for the work area itself changing. Toggling taskbar auto-hide or moving it to another edge leaves the notch where it was until something else re-places it. --- windows/codenotch/src/main.rs | 55 ++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/windows/codenotch/src/main.rs b/windows/codenotch/src/main.rs index f63fecd32..7da7c8786 100644 --- a/windows/codenotch/src/main.rs +++ b/windows/codenotch/src/main.rs @@ -106,6 +106,16 @@ impl Screen { work: (wa.position.x, wa.position.y, wa.size.width as i32, wa.size.height as i32), } } + /// Where the notch may sit. Falls back to the whole monitor if the platform reports no usable + /// work area, which would otherwise pin the notch to (0, 0) with no span to move along. + fn area(&self) -> (i32, i32, i32, i32) { + let (x, y, w, h) = self.work; + if w > 0 && h > 0 { + (x, y, w, h) + } else { + (self.x, self.y, self.w, self.h) + } + } fn contains(&self, x: i32, y: i32) -> bool { x >= self.x && x < self.x + self.w && y >= self.y && y < self.y + self.h } @@ -150,22 +160,28 @@ fn target_screen(app: &AppHandle) -> Option { /// The window's top-left corner for an edge, a position ratio along it and a measured window size. /// `ratio` is the *centre* of the notch along the edge, so 0.5 is the middle whatever the size. +/// The work area, not the monitor: a notch on the edge the taskbar is docked to would otherwise be +/// covered by it. The Mac places against `frame` rather than `visibleFrame` on purpose, but what it +/// overlaps there is the menu bar, which macOS lets a notch cover; the taskbar wins the z-order +/// among topmost windows and is a click target of its own, so it is room lost. fn edge_origin(s: &Screen, edge: &str, ww: i32, wh: i32, ratio: f64) -> (i32, i32) { + let (ax, ay, aw, ah) = s.area(); let along = |span: i32, len: i32| -> i32 { let v = (span as f64 * ratio - len as f64 / 2.0).round() as i32; v.clamp(0, (span - len).max(0)) }; match edge { - "left" => (s.x, s.y + along(s.h, wh)), - "top" => (s.x + along(s.w, ww), s.y), - "bottom" => (s.x + along(s.w, ww), s.y + s.h - wh), - _ => (s.x + s.w - ww, s.y + along(s.h, wh)), + "left" => (ax, ay + along(ah, wh)), + "top" => (ax + along(aw, ww), ay), + "bottom" => (ax + along(aw, ww), ay + ah - wh), + _ => (ax + aw - ww, ay + along(ah, wh)), } } /// How far the taskbar (or anything else outside the work area) covers each side of a window at -/// (x, y, ww, wh), in physical pixels: top, right, bottom, left. The pill keeps its place against -/// the screen edge; only the hover card, which the page can move, is kept clear of these. +/// (x, y, ww, wh), in physical pixels: top, right, bottom, left. `edge_origin` keeps the pill itself +/// out of the taskbar, so what is left here is the window's other three sides — an upright notch is +/// taller than the work area is on a short screen — and the hover card is what the page moves. fn work_insets(s: &Screen, x: i32, y: i32, ww: i32, wh: i32) -> [i32; 4] { let (wx, wy, waw, wah) = s.work; [ @@ -448,10 +464,13 @@ fn drag_begin(app: AppHandle) { ("bottom", (mon.y + mon.h - cy).max(0)), ]; let edge = d.iter().min_by_key(|(_, v)| *v).map(|(e, _)| *e).unwrap_or("right"); + // Measured against the work area, because that is the span `edge_origin` reads the ratio + // back against: on the monitor's, a drop next to the taskbar landed short of the pointer. + let (ax, ay, aw, ah) = mon.area(); let ratio = if config::edge_is_vertical(edge) { - ((cy - mon.y) as f64 / mon.h.max(1) as f64).clamp(0.0, 1.0) + ((cy - ay) as f64 / ah.max(1) as f64).clamp(0.0, 1.0) } else { - ((cx - mon.x) as f64 / mon.w.max(1) as f64).clamp(0.0, 1.0) + ((cx - ax) as f64 / aw.max(1) as f64).clamp(0.0, 1.0) }; { let st = app.state::(); @@ -1625,6 +1644,26 @@ mod tests { assert_eq!(work_insets(&s, 0, 700, 432, 624), [0, 0, 0, 72]); } + /// A notch on the edge the taskbar is docked to used to sit under it. + #[test] + fn the_notch_is_placed_inside_the_work_area() { + // 3200 × 2000 with a 72 px taskbar along the bottom + let s = Screen { name: None, x: 0, y: 0, w: 3200, h: 2000, scale: 1.5, work: (0, 0, 3200, 1928) }; + for edge in ["left", "right", "top", "bottom"] { + let (ww, wh) = if crate::config::edge_is_vertical(edge) { (432, 624) } else { (624, 624) }; + let (x, y) = super::edge_origin(&s, edge, ww, wh, 0.5); + assert!(y + wh <= 1928, "{edge}: ({x},{y}) {ww}x{wh} reaches into the taskbar"); + assert_eq!(work_insets(&s, x, y, ww, wh), [0; 4], "{edge}: nothing covers it"); + } + // A taskbar on the left moves the left edge in, and leaves the right one where it was + let s = Screen { work: (72, 0, 3128, 2000), ..s }; + assert_eq!(super::edge_origin(&s, "left", 432, 624, 0.5).0, 72); + assert_eq!(super::edge_origin(&s, "right", 432, 624, 0.5).0, 3200 - 432); + // Nothing usable reported: the whole monitor, as before + let s = Screen { work: (0, 0, 0, 0), ..s }; + assert_eq!(super::edge_origin(&s, "bottom", 624, 624, 1.0), (3200 - 624, 2000 - 624)); + } + #[test] fn a_flat_notch_is_wide_enough_for_five_rings() { // 5 × 56 px rings + 4 × 14 px gaps + 36 px padding + 2 × 38.7 px fillets + the orb's 28.5 px reach From c3427d8eb7159d3519e625c217b7dacb550aa25e Mon Sep 17 00:00:00 2001 From: Rajat Patra Date: Fri, 18 Sep 2026 23:04:44 +0530 Subject: [PATCH 2/4] Offer the drop zones in the work area too, so a zone means where it lands The notch now clears the taskbar, but the zone offering it that edge was still drawn against the monitor, so the bottom outline promised a place the notch would no longer take. The zones are welded to the overlay window's own edges, so sizing that window to the work area moves all four with it and leaves dropzones.html alone. The edge the pointer picks is read against the same rectangle: the diagonals that decide it now meet the corners of the outlines that were actually drawn. --- windows/codenotch/src/dropzones.rs | 7 +++++-- windows/codenotch/src/main.rs | 10 +++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/windows/codenotch/src/dropzones.rs b/windows/codenotch/src/dropzones.rs index d0c9fd00a..114382488 100644 --- a/windows/codenotch/src/dropzones.rs +++ b/windows/codenotch/src/dropzones.rs @@ -35,10 +35,13 @@ pub fn show(app: &AppHandle, screen: &crate::Screen, zones: &Zones) { let _ = w.show(); return; } + // The work area, not the monitor: the zones are welded to this window's own edges, so sizing it + // to where the notch may actually land is what keeps a zone's promise true on the taskbar's edge. + let (ax, ay, aw, ah) = screen.area(); let builder = WebviewWindowBuilder::new(app, LABEL, WebviewUrl::App("dropzones.html".into())) .title("Codenotch drop zones") - .position(screen.x as f64, screen.y as f64) - .inner_size(screen.w as f64 / screen.scale, screen.h as f64 / screen.scale) + .position(ax as f64, ay as f64) + .inner_size(aw as f64 / screen.scale, ah as f64 / screen.scale) .decorations(false) .transparent(true) .shadow(false) diff --git a/windows/codenotch/src/main.rs b/windows/codenotch/src/main.rs index 7da7c8786..765073e01 100644 --- a/windows/codenotch/src/main.rs +++ b/windows/codenotch/src/main.rs @@ -366,9 +366,13 @@ fn begin_move(app: AppHandle, depth: f64, length: f64) { }; // The overlay is at the monitor's own scale; the notch page is that scale times its size let size = ui_scale(&app); + // Every figure here is the work area's, to match the overlay window and the notch itself: + // the zone drawn on the taskbar's edge has to sit where the notch will, and the edge the + // pointer picks has to be read against the same rectangle the zones are drawn in. + let (ax, ay, aw, ah) = mon.area(); let mut zones = dropzones::Zones { - w: mon.w as f64 / mon.scale, - h: mon.h as f64 / mon.scale, + w: aw as f64 / mon.scale, + h: ah as f64 / mon.scale, depth: depth * size, length: length * size, target: from.clone(), @@ -377,7 +381,7 @@ fn begin_move(app: AppHandle, depth: f64, length: f64) { let mut target = from.clone(); while left_button_down() { if let Ok(cur) = app.cursor_position() { - let next = edge_at(cur.x - mon.x as f64, cur.y - mon.y as f64, mon.w as f64, mon.h as f64); + let next = edge_at(cur.x - ax as f64, cur.y - ay as f64, aw as f64, ah as f64); if next != target { target = next.to_string(); zones.target = target.clone(); From 6cf6b45d5ff860e6cae57af1fa98ce545c0ff585 Mon Sep 17 00:00:00 2001 From: Rajat Patra Date: Fri, 18 Sep 2026 23:08:49 +0530 Subject: [PATCH 3/4] Put the notch back on its edge when the taskbar moves under it Placing the notch against the work area means its position now depends on a number that changes without us: move the taskbar to another edge and the notch stayed where the old work area put it, floating a taskbar's width out in the gap the taskbar had left behind. Nothing hands us WM_SETTINGCHANGE, so the work area is re-read once a second and the notch placed again when it differs. Polled rather than hooked: it changes by hand, a second late is not noticeable, and hooking it would mean subclassing a window we do not own to catch something that happens once in a session. Skipped mid-drag, where the notch is already following the pointer. --- windows/codenotch/src/main.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/windows/codenotch/src/main.rs b/windows/codenotch/src/main.rs index 765073e01..345ee0d3a 100644 --- a/windows/codenotch/src/main.rs +++ b/windows/codenotch/src/main.rs @@ -283,6 +283,37 @@ pub fn place_notch(app: &AppHandle) { } } +/// How often the work area is re-read. It only changes by hand — the taskbar moved to another edge, +/// resized, or switched to auto-hide — so a second late is not noticeable. +const WORK_AREA_POLL_MS: u64 = 1000; + +/// Puts the notch back on its edge when the work area moves under it. +/// +/// Nothing hands us `WM_SETTINGCHANGE`, and the notch is placed against the work area now, so +/// moving the taskbar to another edge would otherwise leave the notch a taskbar's width from the +/// edge it is pinned to, floating in the gap the old taskbar left. Polled rather than hooked, +/// because hooking it means subclassing a window we do not own to catch something that happens +/// once in a session. +fn start_work_area_watch(app: AppHandle) { + std::thread::spawn(move || { + let mut last = target_screen(&app).map(|s| s.work); + loop { + std::thread::sleep(std::time::Duration::from_millis(WORK_AREA_POLL_MS)); + // Mid-drag the notch is following the pointer, and placing it again would fight that. + if DRAGGING.load(std::sync::atomic::Ordering::SeqCst) { + continue; + } + let now = target_screen(&app).map(|s| s.work); + if now == last { + continue; + } + applog(&format!("work area changed: {last:?} -> {now:?}")); + last = now; + place_notch(&app); + } + }); +} + /// Older entry point name still used by tray.rs. Recentre puts the notch in the middle of the edge it /// is on, and only sends it home to the primary monitor's right edge when the screen it was on is /// gone — which is the case the button exists for, and the one where its own edge means nothing. @@ -1579,6 +1610,7 @@ fn main() { let gh = handle.clone(); std::thread::spawn(move || reload_glyphs(&gh)); start_pointer_watchdog(handle.clone()); + start_work_area_watch(handle.clone()); // Seen-clears-it scan let acker = handle.clone(); std::thread::spawn(move || { From b324d7120b102514b9ab4e675ee3fad547d3d2e1 Mon Sep 17 00:00:00 2001 From: Rajat Patra Date: Fri, 18 Sep 2026 23:13:38 +0530 Subject: [PATCH 4/4] Pin the zone overlay in physical pixels, as the notch itself is pinned The work area is physical, and the window builder's position and size are logical, so a work area that does not start at the monitor's own corner was placed a scale factor out: with the taskbar at the top the overlay began a few pixels below the work area and hung off the bottom of the screen by the same amount, trimming the bottom zone. A taskbar on the left or right padded the near side and fell short of the far one. At the bottom the corner is still (0, 0), which is why only that edge looked right. Position and size are asserted again in physical pixels once the window exists, the way place_notch pins the notch, so the overlay lands on the work area whichever corner it starts from. --- windows/codenotch/src/dropzones.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/windows/codenotch/src/dropzones.rs b/windows/codenotch/src/dropzones.rs index 114382488..0d4bfa6a8 100644 --- a/windows/codenotch/src/dropzones.rs +++ b/windows/codenotch/src/dropzones.rs @@ -40,7 +40,7 @@ pub fn show(app: &AppHandle, screen: &crate::Screen, zones: &Zones) { let (ax, ay, aw, ah) = screen.area(); let builder = WebviewWindowBuilder::new(app, LABEL, WebviewUrl::App("dropzones.html".into())) .title("Codenotch drop zones") - .position(ax as f64, ay as f64) + .position(ax as f64 / screen.scale, ay as f64 / screen.scale) .inner_size(aw as f64 / screen.scale, ah as f64 / screen.scale) .decorations(false) .transparent(true) @@ -51,6 +51,12 @@ pub fn show(app: &AppHandle, screen: &crate::Screen, zones: &Zones) { .resizable(false); match builder.build() { Ok(w) => { + // The builder's figures are logical, and Windows converts them with whichever monitor it + // decides the window belongs to — which is how a taskbar anywhere but the bottom left the + // overlay short of the work area's corner and hanging off the far edge by the same amount. + // The work area is physical and absolute, so it is pinned again here, as `place_notch` does. + let _ = w.set_position(tauri::PhysicalPosition::new(ax, ay)); + let _ = w.set_size(tauri::PhysicalSize::new(aw.max(1) as u32, ah.max(1) as u32)); let _ = w.set_ignore_cursor_events(true); // The page asks for the zones itself once it is listening; this covers the other order let _ = w.emit_to(LABEL, "zones", zones);