breadmon/src/layout.rs
Breadway d98b5b4fad
Some checks failed
dev release / build (push) Failing after 0s
Share ~/.config/hypr/monitors.json with the BOS Display panel
Read the file as the initial layout on start. After a successful
hyprctl apply (including applying a named profile), write pretty
JSON so Hyprland and bos-settings stay in sync. Profiles remain
named snapshots under ~/.config/breadmon/profiles/.

Pin bread-utils to bread-ecosystem v0.7.2.
2026-08-15 22:53:28 +08:00

252 lines
7.1 KiB
Rust

use crate::monitor::Monitor;
#[derive(Debug, Clone)]
pub struct LayoutState {
pub selected: usize,
pub snap_threshold: i32,
pub zoom: f32,
}
impl Default for LayoutState {
fn default() -> Self {
Self {
selected: 0,
snap_threshold: 10,
zoom: 1.0,
}
}
}
impl LayoutState {
pub fn clamp_selected(&mut self, count: usize) {
if count == 0 {
self.selected = 0;
} else if self.selected >= count {
self.selected = count - 1;
}
}
pub fn next(&mut self, count: usize) {
if count > 0 {
self.selected = (self.selected + 1) % count;
}
}
pub fn prev(&mut self, count: usize) {
if count > 0 {
self.selected = self.selected.checked_sub(1).unwrap_or(count - 1);
}
}
}
/// Snap a monitor being moved to the edges of its neighbours.
/// Returns the adjusted (x, y) after snapping.
pub fn snap_position(
moving_idx: usize,
new_x: i32,
new_y: i32,
monitors: &[Monitor],
threshold: i32,
) -> (i32, i32) {
let mw = monitors[moving_idx].world_width() as i32;
let mh = monitors[moving_idx].world_height() as i32;
// Candidate snaps: (snapped_x, snapped_y, priority)
// We collect the closest snap in each axis independently
let mut best_x_dist = threshold + 1;
let mut best_y_dist = threshold + 1;
let mut snap_x = new_x;
let mut snap_y = new_y;
for (i, other) in monitors.iter().enumerate() {
if i == moving_idx {
continue;
}
let ox = other.x;
let oy = other.y;
let ow = other.world_width() as i32;
let oh = other.world_height() as i32;
// X-axis edge pairs:
// moving left vs other left
let pairs_x = [
(new_x, ox), // left aligned
(new_x, ox + ow), // moving left snaps to other right
(new_x + mw, ox), // moving right snaps to other left
(new_x + mw, ox + ow), // right aligned
];
for (ma, oa) in pairs_x {
let dist = (ma - oa).abs();
if dist < best_x_dist {
best_x_dist = dist;
snap_x = new_x + (oa - ma);
}
}
// Y-axis edge pairs
let pairs_y = [
(new_y, oy),
(new_y, oy + oh),
(new_y + mh, oy),
(new_y + mh, oy + oh),
];
for (ma, oa) in pairs_y {
let dist = (ma - oa).abs();
if dist < best_y_dist {
best_y_dist = dist;
snap_y = new_y + (oa - ma);
}
}
}
(snap_x, snap_y)
}
/// Move the selected monitor by (dx, dy) pixels, then snap.
pub fn move_selected(state: &LayoutState, monitors: &mut [Monitor], dx: i32, dy: i32) {
let idx = state.selected;
if idx >= monitors.len() {
return;
}
let new_x = monitors[idx].x + dx;
let new_y = monitors[idx].y + dy;
let (sx, sy) = snap_position(idx, new_x, new_y, monitors, state.snap_threshold);
monitors[idx].x = sx;
monitors[idx].y = sy;
}
/// Place monitors in a left-to-right row with no gaps.
pub fn auto_arrange(monitors: &mut [Monitor]) {
let mut cursor = 0i32;
for m in monitors.iter_mut() {
m.x = cursor;
m.y = 0;
cursor += m.world_width() as i32;
}
}
/// Compute canvas scale: returns pixels-per-cell such that all monitors fit in (area_w, area_h) cells.
/// The 0.5 factor corrects for terminal cells being ~2:1 tall.
pub fn canvas_scale(monitors: &[Monitor], area_w: u16, area_h: u16, zoom: f32) -> f32 {
if monitors.is_empty() || area_w == 0 || area_h == 0 {
return 1.0;
}
let (min_x, min_y, max_x, max_y) = bounding_box(monitors);
let total_w = (max_x - min_x).max(1) as f32;
let total_h = (max_y - min_y).max(1) as f32;
let sx = area_w as f32 / total_w;
let sy = area_h as f32 / total_h * 0.5; // cell-aspect correction
sx.min(sy) * zoom
}
/// (min_x, min_y, max_x, max_y) in world coordinates
pub fn bounding_box(monitors: &[Monitor]) -> (i32, i32, i32, i32) {
let min_x = monitors.iter().map(|m| m.x).min().unwrap_or(0);
let min_y = monitors.iter().map(|m| m.y).min().unwrap_or(0);
let max_x = monitors.iter().map(|m| m.right_edge()).max().unwrap_or(1);
let max_y = monitors.iter().map(|m| m.bottom_edge()).max().unwrap_or(1);
(min_x, min_y, max_x, max_y)
}
/// Map a canvas cell position back to world coordinates (inverse of world_to_canvas)
pub fn canvas_to_world(
col: u16,
row: u16,
scale: f32,
origin_x: i32,
origin_y: i32,
pad_x: u16,
pad_y: u16,
) -> (i32, i32) {
let wx = if col >= pad_x {
((col - pad_x) as f32 / scale) as i32 + origin_x
} else {
origin_x - (((pad_x - col) as f32 / scale) as i32)
};
let wy = if row >= pad_y {
((row - pad_y) as f32 / (scale * 0.5)) as i32 + origin_y
} else {
origin_y - (((pad_y - row) as f32 / (scale * 0.5)) as i32)
};
(wx, wy)
}
/// Map a world coordinate to a canvas cell position
pub fn world_to_canvas(
wx: i32,
wy: i32,
scale: f32,
origin_x: i32,
origin_y: i32,
pad_x: u16,
pad_y: u16,
) -> (u16, u16) {
let cx = ((wx - origin_x) as f32 * scale) as u16 + pad_x;
let cy = ((wy - origin_y) as f32 * scale * 0.5) as u16 + pad_y;
(cx, cy)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::{Mode, Transform};
fn mon(name: &str, w: u32, h: u32, x: i32, y: i32) -> Monitor {
Monitor {
name: name.into(),
description: String::new(),
active_mode: Mode {
width: w,
height: h,
refresh: 60.0,
},
x,
y,
scale: 1.0,
transform: Transform::Normal,
vrr: false,
dpms: true,
disabled: false,
mirror_of: None,
available_modes: vec![],
physical_width_mm: 0,
physical_height_mm: 0,
}
}
#[test]
fn snap_to_adjacent_right_edge() {
let monitors = vec![
mon("A", 1920, 1200, 0, 0),
mon("B", 1920, 1080, 1915, 0), // almost touching, 5px gap
];
// Moving B (index 1) at x=1915, A's right edge is 1920
// snap should pull B to x=1920
let (sx, _sy) = snap_position(1, 1915, 0, &monitors, 10);
assert_eq!(sx, 1920);
}
#[test]
fn no_snap_beyond_threshold() {
let monitors = vec![
mon("A", 1920, 1200, 0, 0),
mon("B", 1920, 1080, 1950, 0), // 30px gap, beyond threshold
];
let (sx, _sy) = snap_position(1, 1950, 0, &monitors, 10);
assert_eq!(sx, 1950);
}
#[test]
fn auto_arrange_no_gaps() {
let mut monitors = vec![
mon("A", 1920, 1200, 100, 50),
mon("B", 1280, 1024, 200, 100),
];
auto_arrange(&mut monitors);
assert_eq!(monitors[0].x, 0);
assert_eq!(monitors[0].y, 0);
assert_eq!(monitors[1].x, 1920);
assert_eq!(monitors[1].y, 0);
}
}