Compare commits
1
Commits
8d49a993e9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b9fc8517d |
+134
-10
@@ -504,15 +504,20 @@ impl eframe::App for App {
|
|||||||
let pane = self.panes.get_mut(&id).unwrap();
|
let pane = self.panes.get_mut(&id).unwrap();
|
||||||
let window_id = egui::Id::new(("float", tab.id, id));
|
let window_id = egui::Id::new(("float", tab.id, id));
|
||||||
let active = tab.active == id;
|
let active = tab.active == id;
|
||||||
let (pos, size) = float_placement(workspace, index, &self.config.multiplexer);
|
let window = egui::Window::new(pane_title(id, pane))
|
||||||
let result = egui::Window::new(pane_title(id, pane))
|
|
||||||
.id(window_id)
|
.id(window_id)
|
||||||
.title_bar(false)
|
.title_bar(false)
|
||||||
.constrain_to(workspace)
|
.constrain_to(workspace)
|
||||||
.default_pos(pos)
|
|
||||||
.default_size(size)
|
|
||||||
.min_size(vec2(160.0, 100.0))
|
.min_size(vec2(160.0, 100.0))
|
||||||
.frame(egui::Frame::NONE.fill(bg))
|
.frame(egui::Frame::NONE.fill(bg));
|
||||||
|
let result = position_float(
|
||||||
|
ctx,
|
||||||
|
window,
|
||||||
|
window_id,
|
||||||
|
index,
|
||||||
|
workspace,
|
||||||
|
&self.config.multiplexer,
|
||||||
|
)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
let rect = ui.available_rect_before_wrap();
|
let rect = ui.available_rect_before_wrap();
|
||||||
ui.allocate_rect(rect, egui::Sense::hover());
|
ui.allocate_rect(rect, egui::Sense::hover());
|
||||||
@@ -661,7 +666,7 @@ fn show_pane(
|
|||||||
/// Where a floating pane opens, from its place in the tab's float list rather than
|
/// 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
|
/// 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
|
/// 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.
|
/// every six panes, with each new stack shifted slightly to the right.
|
||||||
fn float_placement(
|
fn float_placement(
|
||||||
workspace: Rect,
|
workspace: Rect,
|
||||||
index: usize,
|
index: usize,
|
||||||
@@ -671,8 +676,43 @@ fn float_placement(
|
|||||||
// terminal far more room than it needs; the maximums keep it in hand.
|
// terminal far more room than it needs; the maximums keep it in hand.
|
||||||
let size = (workspace.size() * vec2(config.floating_width, config.floating_height))
|
let size = (workspace.size() * vec2(config.floating_width, config.floating_height))
|
||||||
.min(vec2(config.floating_max_width, config.floating_max_height));
|
.min(vec2(config.floating_max_width, config.floating_max_height));
|
||||||
|
(workspace.center() - size / 2.0 + float_offset(index), size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn float_offset(index: usize) -> egui::Vec2 {
|
||||||
let step = (index % 6) as f32 * 28.0;
|
let step = (index % 6) as f32 * 28.0;
|
||||||
(workspace.center() - size / 2.0 + vec2(step, step), size)
|
let stack = (index / 6) as f32 * 12.0;
|
||||||
|
vec2(step + stack, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reapply the cascade when a pane changes slots or the workspace resizes.
|
||||||
|
/// Otherwise leave egui's remembered position alone so panes can still be dragged.
|
||||||
|
fn position_float<'a>(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
window: egui::Window<'a>,
|
||||||
|
window_id: egui::Id,
|
||||||
|
index: usize,
|
||||||
|
workspace: Rect,
|
||||||
|
config: &config::Multiplexer,
|
||||||
|
) -> egui::Window<'a> {
|
||||||
|
let (pos, size) = float_placement(workspace, index, config);
|
||||||
|
let window = window.default_pos(pos).default_size(size);
|
||||||
|
let changed = ctx.data_mut(|data| {
|
||||||
|
let key = window_id.with("cascade placement");
|
||||||
|
let placement = (index, workspace);
|
||||||
|
let previous = data.get_temp::<(usize, Rect)>(key);
|
||||||
|
data.insert_temp(key, placement);
|
||||||
|
previous.is_some_and(|previous| previous != placement)
|
||||||
|
});
|
||||||
|
if changed {
|
||||||
|
// Center using the remembered size, including user resizing.
|
||||||
|
let size = egui::AreaState::load(ctx, window_id)
|
||||||
|
.and_then(|state| state.size)
|
||||||
|
.unwrap_or(size);
|
||||||
|
window.current_pos(workspace.center() - size / 2.0 + float_offset(index))
|
||||||
|
} else {
|
||||||
|
window
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pane_title(id: PaneId, pane: &Pane) -> String {
|
fn pane_title(id: PaneId, pane: &Pane) -> String {
|
||||||
@@ -749,6 +789,80 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removing_floats_repositions_survivors_in_cycle_order() {
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
let config = config::Multiplexer::default();
|
||||||
|
let workspace = Rect::from_min_size(egui::Pos2::ZERO, vec2(1600.0, 1200.0));
|
||||||
|
let mut tab = Tab::new(1, 1);
|
||||||
|
tab.floats = vec![2, 3, 4, 5];
|
||||||
|
tab.floats_visible = true;
|
||||||
|
let render = |tab: &Tab, moved: Option<PaneId>, workspace: Rect| {
|
||||||
|
let mut positions = BTreeMap::new();
|
||||||
|
// Allow egui's initial sizing pass to settle.
|
||||||
|
for _ in 0..3 {
|
||||||
|
let _ = ctx.run(
|
||||||
|
egui::RawInput {
|
||||||
|
screen_rect: Some(workspace),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|ctx| {
|
||||||
|
for (index, &id) in tab.floats.iter().enumerate() {
|
||||||
|
let window_id = egui::Id::new(("float", tab.id, id));
|
||||||
|
let mut window = egui::Window::new(id.to_string())
|
||||||
|
.id(window_id)
|
||||||
|
.title_bar(false)
|
||||||
|
.frame(egui::Frame::NONE);
|
||||||
|
if moved == Some(id) {
|
||||||
|
window = window.current_pos(egui::pos2(100.0, 100.0));
|
||||||
|
}
|
||||||
|
let response =
|
||||||
|
position_float(ctx, window, window_id, index, workspace, &config)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.allocate_rect(
|
||||||
|
ui.available_rect_before_wrap(),
|
||||||
|
egui::Sense::hover(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
positions.insert(id, response.response.rect.min);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
positions
|
||||||
|
};
|
||||||
|
render(&tab, Some(2), workspace);
|
||||||
|
assert_eq!(render(&tab, None, workspace)[&2], egui::pos2(100.0, 100.0));
|
||||||
|
// Resizing recenters even a dragged pane, using its existing size.
|
||||||
|
let resized = Rect::from_min_size(egui::Pos2::ZERO, vec2(1800.0, 1400.0));
|
||||||
|
let positions = render(&tab, None, resized);
|
||||||
|
for (index, &id) in tab.floats.iter().enumerate() {
|
||||||
|
assert_eq!(positions[&id], float_placement(resized, index, &config).0);
|
||||||
|
}
|
||||||
|
render(&tab, None, workspace);
|
||||||
|
render(&tab, Some(2), workspace);
|
||||||
|
// Remove the middle pane, then the first, then append a new float.
|
||||||
|
for (removed, added) in [(3, None), (2, None), (4, Some(6))] {
|
||||||
|
tab.detach(removed);
|
||||||
|
if let Some(id) = added {
|
||||||
|
tab.floats.push(id);
|
||||||
|
}
|
||||||
|
let positions = render(&tab, None, workspace);
|
||||||
|
tab.active = tab.floats[0];
|
||||||
|
for index in 0..tab.floats.len() {
|
||||||
|
let expected = if tab.active == 2 {
|
||||||
|
egui::pos2(100.0, 100.0)
|
||||||
|
} else {
|
||||||
|
float_placement(workspace, index, &config).0
|
||||||
|
};
|
||||||
|
assert_eq!(positions[&tab.active], expected);
|
||||||
|
tab.cycle(false);
|
||||||
|
}
|
||||||
|
assert_eq!(tab.active, tab.floats[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn floats_open_centered_then_cascade_and_stay_within_the_maximums() {
|
fn floats_open_centered_then_cascade_and_stay_within_the_maximums() {
|
||||||
let config = config::Multiplexer::default();
|
let config = config::Multiplexer::default();
|
||||||
@@ -770,9 +884,19 @@ mod tests {
|
|||||||
second.x > first.x && second.y > first.y,
|
second.x > first.x && second.y > first.y,
|
||||||
"later floats step off it"
|
"later floats step off it"
|
||||||
);
|
);
|
||||||
// Six slots later the cascade is back where it started, so it cannot walk away.
|
// Each group of six restarts the cascade slightly to the right.
|
||||||
assert_eq!(float_placement(workspace, 6, &config).0, first);
|
assert_eq!(
|
||||||
assert_eq!(float_placement(workspace, 7, &config).0, second);
|
float_placement(workspace, 6, &config).0,
|
||||||
|
first + vec2(12.0, 0.0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
float_placement(workspace, 7, &config).0,
|
||||||
|
second + vec2(12.0, 0.0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
float_placement(workspace, 12, &config).0,
|
||||||
|
first + vec2(24.0, 0.0)
|
||||||
|
);
|
||||||
|
|
||||||
// A small workspace keeps the fractions instead of the maximums.
|
// 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 small = Rect::from_min_size(egui::Pos2::ZERO, vec2(800.0, 600.0));
|
||||||
|
|||||||
+58
-7
@@ -152,7 +152,8 @@ pub struct Tab {
|
|||||||
pub floats: Vec<PaneId>,
|
pub floats: Vec<PaneId>,
|
||||||
pub active: PaneId,
|
pub active: PaneId,
|
||||||
pub floats_visible: bool,
|
pub floats_visible: bool,
|
||||||
last_active_float: Option<PaneId>,
|
// Remember both focus and zoom while floating panes are hidden.
|
||||||
|
last_active_float: Option<(PaneId, bool)>,
|
||||||
pub zoomed: bool,
|
pub zoomed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +180,7 @@ impl Tab {
|
|||||||
pub fn detach(&mut self, id: PaneId) {
|
pub fn detach(&mut self, id: PaneId) {
|
||||||
self.root = self.root.take().and_then(|r| r.remove(id));
|
self.root = self.root.take().and_then(|r| r.remove(id));
|
||||||
self.floats.retain(|p| *p != id);
|
self.floats.retain(|p| *p != id);
|
||||||
if self.last_active_float == Some(id) {
|
if self.last_active_float.is_some_and(|(pane, _)| pane == id) {
|
||||||
self.last_active_float = None;
|
self.last_active_float = None;
|
||||||
}
|
}
|
||||||
if self.floats.is_empty() {
|
if self.floats.is_empty() {
|
||||||
@@ -204,17 +205,18 @@ impl Tab {
|
|||||||
|
|
||||||
pub fn toggle_floats(&mut self) {
|
pub fn toggle_floats(&mut self) {
|
||||||
self.floats_visible = !self.floats_visible;
|
self.floats_visible = !self.floats_visible;
|
||||||
self.zoomed = false;
|
let was_zoomed = std::mem::take(&mut self.zoomed);
|
||||||
if self.floats_visible {
|
if self.floats_visible {
|
||||||
if let Some(id) = self
|
if let Some((id, zoomed)) = self
|
||||||
.last_active_float
|
.last_active_float
|
||||||
.filter(|id| self.floats.contains(id))
|
.filter(|(id, _)| self.floats.contains(id))
|
||||||
.or_else(|| self.floats.last().copied())
|
.or_else(|| self.floats.last().map(|id| (*id, false)))
|
||||||
{
|
{
|
||||||
self.active = id;
|
self.active = id;
|
||||||
|
self.zoomed = zoomed;
|
||||||
}
|
}
|
||||||
} else if self.floats.contains(&self.active) {
|
} else if self.floats.contains(&self.active) {
|
||||||
self.last_active_float = Some(self.active);
|
self.last_active_float = Some((self.active, was_zoomed));
|
||||||
if let Some(root) = &self.root {
|
if let Some(root) = &self.root {
|
||||||
self.active = root.first();
|
self.active = root.first();
|
||||||
}
|
}
|
||||||
@@ -374,6 +376,7 @@ mod tests {
|
|||||||
tab.floats = vec![2, 3];
|
tab.floats = vec![2, 3];
|
||||||
tab.floats_visible = true;
|
tab.floats_visible = true;
|
||||||
tab.active = 2;
|
tab.active = 2;
|
||||||
|
tab.zoomed = true;
|
||||||
tab.toggle_floats();
|
tab.toggle_floats();
|
||||||
tab.detach(2);
|
tab.detach(2);
|
||||||
if dock {
|
if dock {
|
||||||
@@ -381,9 +384,57 @@ mod tests {
|
|||||||
}
|
}
|
||||||
tab.toggle_floats();
|
tab.toggle_floats();
|
||||||
assert_eq!(tab.active, 3);
|
assert_eq!(tab.active, 3);
|
||||||
|
assert!(!tab.zoomed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn showing_floats_restores_zoom_without_zooming_tiles() {
|
||||||
|
let mut tab = Tab::new(1, 1);
|
||||||
|
tab.root.as_mut().unwrap().split(1, 2, Axis::Horizontal);
|
||||||
|
tab.floats = vec![3, 4];
|
||||||
|
tab.floats_visible = true;
|
||||||
|
tab.active = 3;
|
||||||
|
tab.zoomed = true;
|
||||||
|
|
||||||
|
for _ in 0..2 {
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 1);
|
||||||
|
assert!(!tab.zoomed);
|
||||||
|
assert!(!tab.is_visible(3));
|
||||||
|
tab.cycle(false);
|
||||||
|
assert_eq!(tab.active, 2);
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 3);
|
||||||
|
assert!(tab.zoomed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unzooming the float replaces its remembered zoom, even if tiles zoom.
|
||||||
|
tab.zoomed = false;
|
||||||
|
tab.toggle_floats();
|
||||||
|
tab.zoomed = true;
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 3);
|
||||||
|
assert!(!tab.zoomed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn floating_zoom_survives_hiding_without_tiles() {
|
||||||
|
let mut tab = Tab::new(1, 1);
|
||||||
|
tab.root = None;
|
||||||
|
tab.floats = vec![1];
|
||||||
|
tab.floats_visible = true;
|
||||||
|
tab.zoomed = true;
|
||||||
|
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert!(!tab.is_visible(1));
|
||||||
|
assert!(!tab.zoomed);
|
||||||
|
tab.toggle_floats();
|
||||||
|
assert_eq!(tab.active, 1);
|
||||||
|
assert!(tab.is_visible(1));
|
||||||
|
assert!(tab.zoomed);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn directional_focus_uses_geometry_and_ignores_hidden_or_foreign_panes() {
|
fn directional_focus_uses_geometry_and_ignores_hidden_or_foreign_panes() {
|
||||||
let mut tab = Tab::new(1, 1);
|
let mut tab = Tab::new(1, 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user