diff --git a/config.example.toml b/config.example.toml index 4113a43..a89b2fd 100644 --- a/config.example.toml +++ b/config.example.toml @@ -21,6 +21,9 @@ allow_shared_memory = true max_panes = 32 floating_width = 0.65 floating_height = 0.65 +# Fractions of the workspace, capped at these many points on large displays. +floating_max_width = 960 +floating_max_height = 640 # Providing [keys] replaces the entire default map. [keys] diff --git a/result b/result index ebd0f67..6469c55 120000 --- a/result +++ b/result @@ -1 +1 @@ -/nix/store/cq7bwr3w31mdn9rp3sa0isv5i4c6awqg-slerm-0.1.0 \ No newline at end of file +/nix/store/f3ssbg9n2ykvy4n6wslfgl25gr8pzkbl-slerm-0.1.0 \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index f967def..a9c15db 100644 --- a/src/app.rs +++ b/src/app.rs @@ -432,25 +432,17 @@ impl eframe::App for App { } }); if tab.floats_visible && !tab.zoomed { - for id in tab.floats.clone() { + for (index, id) in tab.floats.clone().into_iter().enumerate() { let pane = self.panes.get_mut(&id).unwrap(); let window_id = egui::Id::new(("float", tab.id, id)); let active = tab.active == id; + let (pos, size) = float_placement(workspace, index, &self.config.multiplexer); let result = egui::Window::new(pane_title(id, pane)) .id(window_id) .title_bar(false) .constrain_to(workspace) - .default_pos( - workspace.min - + vec2(40.0 + (id % 5) as f32 * 22.0, 40.0 + (id % 5) as f32 * 22.0), - ) - .default_size( - workspace.size() - * vec2( - self.config.multiplexer.floating_width, - self.config.multiplexer.floating_height, - ), - ) + .default_pos(pos) + .default_size(size) .min_size(vec2(160.0, 100.0)) .frame(egui::Frame::NONE.fill(bg)) .show(ctx, |ui| { @@ -569,6 +561,23 @@ fn show_pane( focused } +/// Where a floating pane opens, from its place in the tab's float list rather than +/// its pane id: the first one is centred and every later one steps down and right +/// from it, so the same slot always lands in the same spot. The cascade wraps +/// before it can walk a float off the far edge. +fn float_placement( + workspace: Rect, + index: usize, + config: &config::Multiplexer, +) -> (egui::Pos2, egui::Vec2) { + // The fractions are of the workspace, so on a large display they hand a scratch + // terminal far more room than it needs; the maximums keep it in hand. + let size = (workspace.size() * vec2(config.floating_width, config.floating_height)) + .min(vec2(config.floating_max_width, config.floating_max_height)); + let step = (index % 6) as f32 * 28.0; + (workspace.center() - size / 2.0 + vec2(step, step), size) +} + fn pane_title(id: PaneId, pane: &Pane) -> String { let title = pane.terminal.title().unwrap_or(""); format!( @@ -640,6 +649,41 @@ mod tests { ); } + #[test] + fn floats_open_centered_then_cascade_and_stay_within_the_maximums() { + let config = config::Multiplexer::default(); + let workspace = Rect::from_min_size(egui::pos2(0.0, 30.0), vec2(3000.0, 1800.0)); + let (first, size) = float_placement(workspace, 0, &config); + assert_eq!( + size, + vec2(config.floating_max_width, config.floating_max_height), + "a large workspace must not hand out a proportionally large float" + ); + assert_eq!( + first + size / 2.0, + workspace.center(), + "the first is centered" + ); + + let (second, _) = float_placement(workspace, 1, &config); + assert!( + second.x > first.x && second.y > first.y, + "later floats step off it" + ); + // Six slots later the cascade is back where it started, so it cannot walk away. + assert_eq!(float_placement(workspace, 6, &config).0, first); + assert_eq!(float_placement(workspace, 7, &config).0, second); + + // A small workspace keeps the fractions instead of the maximums. + let small = Rect::from_min_size(egui::Pos2::ZERO, vec2(800.0, 600.0)); + let (pos, size) = float_placement(small, 0, &config); + assert_eq!( + size, + small.size() * vec2(config.floating_width, config.floating_height) + ); + assert_eq!(pos + size / 2.0, small.center()); + } + #[test] fn toggle_creates_first_float_then_reuses_it_and_close_removes_focus() { let ctx = egui::Context::default(); diff --git a/src/config.rs b/src/config.rs index 4d5c885..5cbcc6d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,6 +40,8 @@ pub struct Multiplexer { pub max_panes: usize, pub floating_width: f32, pub floating_height: f32, + pub floating_max_width: f32, + pub floating_max_height: f32, } #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] @@ -83,6 +85,10 @@ impl Default for Multiplexer { max_panes: 32, floating_width: 0.65, floating_height: 0.65, + // The fractions alone make a float fill half of a large display, which is + // far more than a scratch terminal needs, so cap them in points too. + floating_max_width: 960.0, + floating_max_height: 640.0, } } } @@ -181,6 +187,15 @@ impl Config { "floating dimensions must be fractions in 0.2..0.95" ); } + for points in [ + config.multiplexer.floating_max_width, + config.multiplexer.floating_max_height, + ] { + ensure!( + (160.0..=16384.0).contains(&points), + "floating maximums must be 160..16384 points" + ); + } for color in [&config.background, &config.foreground, &config.accent] { rgb(color)?; }