Skip to content

024 — Display subsystem: coordinate bugs + module refactor #24

@archae0pteryx

Description

@archae0pteryx

Confirmed facts (runtime)

  1. Single monitor works — pig appears on the selected monitor correctly.
  2. With both selected, pigs hit a barrier in the middle (not at a monitor edge) when walking and when dragged.
  3. Pigs walk off the right edge of one display and off the left edge of the other — no boundary there. They disappear.
  4. Display names in tray are identical — no disambiguation.
  5. Hardware: 270° rotated portrait monitor to the left of the main landscape display.

Root cause

Mixed-unit bounding box (core bug)

overlay_manager::apply computes the spanning window bounds by mixing logical positions with physical sizes:

// Current — WRONG
let min_x = enabled.iter().map(|m| m.position.x).min();           // logical pixels
let max_x = enabled.iter().map(|m| m.position.x + m.size.width as i32).max(); // logical + physical

Monitor::position() on macOS returns logical coordinates (points).
Monitor::size() returns physical pixels.
At 2× DPR: a 1920-wide monitor has position.x = 1920 (logical) but size.width = 3840 (physical).
max_x = 1920 + 3840 = 5760 → wrong. Correct is 3840 + 3840 = 7680 physical, or 1920 + 1920 = 3840 logical.

The window ends up smaller than the actual span and offset inside it.
Result: open edges on the outside, invisible barrier in the middle.

Rotated monitor compounds the error

macOS reports the 270°-rotated portrait monitor with swapped logical dimensions (height > width).
The position origin for a rotated monitor may be anchored at a non-top-left corner in the global coordinate frame.
Current code makes no attempt to handle orientation — bounding box is further wrong for the portrait monitor.

Hit-test thread unit mismatch

let origin = win_clone.outer_position();   // may return logical on macOS
let local_x = cursor.x - origin.x;        // cursor is physical

Same logical/physical confusion in the click-through polling thread.

Fix: display/ module tree

Replace app/overlay_manager.rs and app/pig_hittest.rs with a dedicated display/ module:

src-tauri/src/display/
  mod.rs        — DisplayManager (public surface; re-exports pub types; impls RectUpdater)
  monitor.rs    — pure types + math: LogicalMonitor, compute_span, disambiguate_names
  overlay.rs    — window lifecycle: create, resize, show, destroy
  hit_test.rs   — PigHitTester + polling thread (move from app/pig_hittest.rs)

display/monitor.rs — pure, fully testable

pub struct LogicalMonitor {
    pub index: usize,
    pub label: String,          // disambiguated display name
    pub scale_factor: f64,
    pub position: (f64, f64),   // logical pixels, top-left origin
    pub size: (f64, f64),       // logical pixels (width, height)
}

impl LogicalMonitor {
    /// Normalise a Tauri Monitor: divide physical size+position by scale_factor.
    pub fn from_tauri(index: usize, m: &tauri::Monitor) -> Self;
}

pub struct SpanBounds {
    pub x: f64,       // logical
    pub y: f64,
    pub width: f64,
    pub height: f64,
}

/// Union bounding box of all monitors in logical pixels.
pub fn compute_span(monitors: &[LogicalMonitor]) -> SpanBounds;

/// Append " (x,y)" to names that appear more than once.
pub fn disambiguate_names(monitors: &mut [LogicalMonitor]);

All arithmetic stays in logical space. Window calls use LogicalSize/LogicalPosition — Tauri converts to physical internally.

display/hit_test.rs

Move app/pig_hittest.rs here unchanged. Update polling thread to use outer_position() converted to logical (divide by window's scale factor) so it matches CSS-pixel pig rects.

display/overlay.rs

Extract window create/resize/show/destroy from overlay_manager::ensure_shown. Uses LogicalSize + LogicalPosition for all window calls.

display/mod.rs — DisplayManager

pub struct DisplayManager { entries: Arc<Mutex<HashMap<String, OverlayEntry>>> }
pub struct DisplayManagerState(pub DisplayManager);

impl DisplayManager {
    pub fn apply<R>(&self, app: &AppHandle<R>, monitors: &[LogicalMonitor], config: &DisplayConfig);
}

impl RectUpdater for DisplayManager { ... }

What moves / stays

Symbol From To
PigHitTester app/pig_hittest.rs display/hit_test.rs
OverlayManager app/overlay_manager.rs display/mod.rs as DisplayManager
MonitorInfo app/overlay_manager.rs display/monitor.rs as LogicalMonitor
Bounding-box math app/overlay_manager.rs display/monitor.rs::compute_span
Window lifecycle app/overlay_manager.rs display/overlay.rs
MonitorsState app/mod.rs stays, wraps Vec<LogicalMonitor>
DisplayConfigState app/mod.rs stays
Tray display menu app/tray.rs stays, uses LogicalMonitor.label
PigHitState ui_bridge/mod.rs stays
update_pig_rects ui_bridge/mod.rs stays
DisplayConfig, PigRect, RectUpdater crates/domain stays

Test plan (pure logic in monitor.rs)

  • compute_span single monitor → bounds equal monitor bounds
  • compute_span two landscape monitors side by side → width = sum of widths, x = leftmost
  • compute_span portrait monitor (h > w) to the left of landscape → correct origin + total width
  • compute_span monitor at negative x (secondary left of primary) → min_x is negative
  • disambiguate_names identical names → suffix with "(x, y)"
  • disambiguate_names unique names → unchanged

Acceptance criteria

  • display/ module tree exists; app/overlay_manager.rs and app/pig_hittest.rs removed
  • compute_span and disambiguate_names covered by unit tests
  • With both monitors enabled, pigs roam the full logical span — no barrier in the middle, no open edges
  • Portrait/rotated monitor included correctly in the span
  • Monitor names disambiguated in tray
  • Click-through hit-testing works across the full spanning window
  • Drag works anywhere on the spanning window
  • App launches at runtime without crash
  • task check green

Files touched

  • src-tauri/src/display/ — new module tree (4 files)
  • src-tauri/src/app/overlay_manager.rs — deleted
  • src-tauri/src/app/pig_hittest.rs — deleted
  • src-tauri/src/app/mod.rs — update imports, MonitorInfo → LogicalMonitor
  • src-tauri/src/app/tray.rs — update display menu to use LogicalMonitor.label
  • src-tauri/src/main.rs or lib.rs — add mod display

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions