fix: narrow tree resize presentation dependencies (#41)
* fix: scope tree resize waits to overlapping presentation motion * Keep tree resize waits conservative during spring reversals
This commit is contained in:
1 parent
a5e8ba65e2
commit
216289d66c
2 files changed
+438
-3
No files matched your search
@@ -30,6 +30,106 @@ pub(super) struct ViewportDependencies {
|
|||||||
|
|
||||||
const MAX_VIEWPORT_STEPS: usize = 32;
|
const MAX_VIEWPORT_STEPS: usize = 32;
|
||||||
|
|
||||||
|
/// Connect moving tiles where their conservative swept bounds overlap. Such
|
||||||
|
/// tiles must hand off together: advancing one into a neighbor's
|
||||||
|
/// still-presented source would break the resize/reflow boundary. A fixed
|
||||||
|
/// boundary between independent branches merely touches and creates no edge.
|
||||||
|
/// Use solved presentation rectangles, so constraints, gaps and interrupted
|
||||||
|
/// motion participate rather than assuming structural siblings are independent.
|
||||||
|
fn tree_reflow_groups(
|
||||||
|
motions: impl IntoIterator<Item = (WindowId, Rect, Rect, ferese_animation::RectVelocity)>,
|
||||||
|
inner_gap: f64,
|
||||||
|
allow_overshoot: bool,
|
||||||
|
) -> HashMap<WindowId, usize> {
|
||||||
|
let motions = motions
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, source, target, velocity)| {
|
||||||
|
source != target
|
||||||
|
|| [velocity.x, velocity.y, velocity.width, velocity.height]
|
||||||
|
.into_iter()
|
||||||
|
.any(|speed| speed != 0.0)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
// Endpoint bounds cannot contain an overshooting spring or a retargeted
|
||||||
|
// component still moving away from its destination. Keep the moving tiles
|
||||||
|
// together until their trajectories can be bounded by their endpoints.
|
||||||
|
let reversing = motions.iter().any(|(_, source, target, velocity)| {
|
||||||
|
[
|
||||||
|
(target.x - source.x, velocity.x),
|
||||||
|
(target.y - source.y, velocity.y),
|
||||||
|
(target.width - source.width, velocity.width),
|
||||||
|
(target.height - source.height, velocity.height),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.any(|(distance, speed)| speed != 0.0 && distance * speed <= 0.0)
|
||||||
|
});
|
||||||
|
if allow_overshoot || reversing {
|
||||||
|
return motions.into_iter().map(|(id, ..)| (id, 0)).collect();
|
||||||
|
}
|
||||||
|
let margin = inner_gap.max(0.0) / 2.0;
|
||||||
|
let moving = motions
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, source, target, _)| source != target)
|
||||||
|
.map(|(id, source, target, velocity)| {
|
||||||
|
let left = source.x.min(target.x) - margin;
|
||||||
|
let top = source.y.min(target.y) - margin;
|
||||||
|
// Geometry uses one no-crossing spring for its components. With
|
||||||
|
// proportional velocities an edge shares their progress, including
|
||||||
|
// a fixed split boundary. Interrupted components can have different
|
||||||
|
// progress: bound each separately rather than miss an intermediate
|
||||||
|
// excursion of x+width or y+height beyond both endpoint rectangles.
|
||||||
|
let edge =
|
||||||
|
|position: f64, size: f64, target_position: f64, target_size: f64, speed: f64, size_speed: f64| {
|
||||||
|
if (speed * (target_size - size) - size_speed * (target_position - position)).abs() < 1e-6 {
|
||||||
|
(position + size).max(target_position + target_size)
|
||||||
|
} else {
|
||||||
|
position.max(target_position) + size.max(target_size)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let right = edge(
|
||||||
|
source.x,
|
||||||
|
source.width,
|
||||||
|
target.x,
|
||||||
|
target.width,
|
||||||
|
velocity.x,
|
||||||
|
velocity.width,
|
||||||
|
) + margin;
|
||||||
|
let bottom = edge(
|
||||||
|
source.y,
|
||||||
|
source.height,
|
||||||
|
target.y,
|
||||||
|
target.height,
|
||||||
|
velocity.y,
|
||||||
|
velocity.height,
|
||||||
|
) + margin;
|
||||||
|
(id, Rect::new(left, top, right - left, bottom - top))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut groups = HashMap::new();
|
||||||
|
for (id, _) in &moving {
|
||||||
|
if groups.contains_key(id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let group = groups.len();
|
||||||
|
groups.insert(*id, group);
|
||||||
|
let mut frontier = vec![*id];
|
||||||
|
while let Some(id) = frontier.pop() {
|
||||||
|
let bounds = moving.iter().find(|(window, _)| *window == id).unwrap().1;
|
||||||
|
for (other, candidate) in &moving {
|
||||||
|
if !groups.contains_key(other)
|
||||||
|
&& bounds.x.max(candidate.x) + 0.001 < (bounds.x + bounds.width).min(candidate.x + candidate.width)
|
||||||
|
&& bounds.y.max(candidate.y) + 0.001
|
||||||
|
< (bounds.y + bounds.height).min(candidate.y + candidate.height)
|
||||||
|
{
|
||||||
|
groups.insert(*other, group);
|
||||||
|
frontier.push(*other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups
|
||||||
|
}
|
||||||
|
|
||||||
/// The buffer-dependent properties in this path. Translation has its own
|
/// The buffer-dependent properties in this path. Translation has its own
|
||||||
/// inputs; it does not inherit a tile's resize/reflow wait by membership.
|
/// inputs; it does not inherit a tile's resize/reflow wait by membership.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -268,6 +368,37 @@ impl Ferese {
|
|||||||
let mut reflow = HashMap::new();
|
let mut reflow = HashMap::new();
|
||||||
let mut viewport = HashMap::new();
|
let mut viewport = HashMap::new();
|
||||||
let mut previous_viewport = std::mem::take(&mut self.presentation_dependencies.viewport);
|
let mut previous_viewport = std::mem::take(&mut self.presentation_dependencies.viewport);
|
||||||
|
let tree_groups = self
|
||||||
|
.workspaces
|
||||||
|
.iter()
|
||||||
|
.filter_map(|workspace| {
|
||||||
|
let WorkspaceLayout::Tree(tree) = &workspace.layout else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let motions = tree.window_ids().filter_map(|id| {
|
||||||
|
let record = self.windows.record(id)?;
|
||||||
|
// Output-owned geometry does not participate in tiled reflow.
|
||||||
|
if workspace.fullscreen == Some(id) || record.maximized {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let geometry = record.geometry?;
|
||||||
|
Some((
|
||||||
|
id,
|
||||||
|
geometry.visual.current,
|
||||||
|
geometry.visual.target,
|
||||||
|
geometry.visual.velocity,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
Some((
|
||||||
|
workspace.id,
|
||||||
|
tree_reflow_groups(
|
||||||
|
motions,
|
||||||
|
self.gap_config.inner,
|
||||||
|
self.spring_config.crossing == ferese_animation::CrossingPolicy::AllowOvershoot,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
for (&id, _) in self.windows.records() {
|
for (&id, _) in self.windows.records() {
|
||||||
let Some(workspace_id) = self.workspaces.workspace_for_window(id) else {
|
let Some(workspace_id) = self.workspaces.workspace_for_window(id) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -306,9 +437,9 @@ impl Ferese {
|
|||||||
source == own || (source < own && transaction.width_changes())
|
source == own || (source < own && transaction.width_changes())
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
WorkspaceLayout::Tree(_) => {
|
WorkspaceLayout::Tree(_) => tree_groups.get(&workspace_id).is_some_and(|groups| {
|
||||||
workspace.layout.contains(id) && workspace.layout.contains(*slow)
|
groups.get(&id).is_some_and(|group| groups.get(slow) == Some(group))
|
||||||
}
|
}),
|
||||||
});
|
});
|
||||||
if linked {
|
if linked {
|
||||||
waits.push(commit);
|
waits.push(commit);
|
||||||
@@ -366,6 +497,193 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::resize_transaction::ResizeTransaction;
|
use crate::resize_transaction::ResizeTransaction;
|
||||||
|
|
||||||
|
fn tree_reflow_groups(
|
||||||
|
motions: impl IntoIterator<Item = (WindowId, Rect, Rect)>,
|
||||||
|
inner_gap: f64,
|
||||||
|
) -> HashMap<WindowId, usize> {
|
||||||
|
super::tree_reflow_groups(
|
||||||
|
motions
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, source, target)| (id, source, target, Default::default())),
|
||||||
|
inner_gap,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interrupted_components_use_conservative_edge_bounds() {
|
||||||
|
let source = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||||
|
let target = Rect::new(40.0, 0.0, 60.0, 100.0);
|
||||||
|
let neighbor = Rect::new(110.0, 0.0, 100.0, 100.0);
|
||||||
|
let moving_neighbor = Rect::new(110.0, 0.0, 110.0, 100.0);
|
||||||
|
let groups = super::tree_reflow_groups(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
WindowId(1),
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
ferese_animation::RectVelocity {
|
||||||
|
x: 100.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(WindowId(2), neighbor, moving_neighbor, Default::default()),
|
||||||
|
],
|
||||||
|
0.0,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert_eq!(groups[&WindowId(1)], groups[&WindowId(2)]);
|
||||||
|
let groups = tree_reflow_groups(
|
||||||
|
[(WindowId(1), source, target), (WindowId(2), neighbor, moving_neighbor)],
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
groups[&WindowId(1)],
|
||||||
|
groups[&WindowId(2)],
|
||||||
|
"coherent fixed edge remains independent"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nested_tree() -> ferese_layout::LayoutTree {
|
||||||
|
let mut tree = ferese_layout::LayoutTree::default();
|
||||||
|
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||||
|
tree.insert(WindowId(3), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Vertical, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
tree.insert(WindowId(4), Some(WindowId(3)), Axis::Vertical, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
tree
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn solved_tree_motion_links_shared_boundaries_but_not_independent_branches() {
|
||||||
|
let bounds = Rect::new(0.0, 0.0, 800.0, 600.0);
|
||||||
|
for gap in [0.0, 10.0, 80.0] {
|
||||||
|
let gaps = GapConfig {
|
||||||
|
inner: gap,
|
||||||
|
outer: 10.0,
|
||||||
|
smart: false,
|
||||||
|
};
|
||||||
|
let mut tree = nested_tree();
|
||||||
|
let solve = |tree: &ferese_layout::LayoutTree, constraints: &HashMap<WindowId, SizeConstraints>| {
|
||||||
|
tree.geometry_with_constraints(bounds, gaps, constraints, Some(WindowId(1)))
|
||||||
|
.unwrap()
|
||||||
|
.geometry
|
||||||
|
};
|
||||||
|
let constraints = HashMap::new();
|
||||||
|
let source = solve(&tree, &constraints);
|
||||||
|
tree.resize_window(WindowId(1), Direction::Down, 0.1).unwrap();
|
||||||
|
tree.resize_window(WindowId(3), Direction::Down, 0.1).unwrap();
|
||||||
|
let target = solve(&tree, &constraints);
|
||||||
|
let group = |source: &HashMap<WindowId, Rect>, target: &HashMap<WindowId, Rect>| {
|
||||||
|
tree_reflow_groups(
|
||||||
|
source
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(&id, &rect)| Some((id, rect, *target.get(&id)?))),
|
||||||
|
gap,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let groups = group(&source, &target);
|
||||||
|
assert_eq!(groups[&WindowId(1)], groups[&WindowId(2)]);
|
||||||
|
assert_eq!(groups[&WindowId(3)], groups[&WindowId(4)]);
|
||||||
|
assert_ne!(groups[&WindowId(1)], groups[&WindowId(3)]);
|
||||||
|
|
||||||
|
// A horizontal request walks through the vertical child to the
|
||||||
|
// root split: both branches' actual x/width now participate.
|
||||||
|
tree.resize_window(WindowId(1), Direction::Right, 0.1).unwrap();
|
||||||
|
let ancestor = solve(&tree, &constraints);
|
||||||
|
let groups = group(&source, &ancestor);
|
||||||
|
assert!(groups.values().all(|group| *group == groups[&WindowId(1)]));
|
||||||
|
|
||||||
|
// A minimum in a descendant can move an ancestor boundary even
|
||||||
|
// though no ancestor ratio was explicitly changed.
|
||||||
|
let tree = nested_tree();
|
||||||
|
let constrained = solve(
|
||||||
|
&tree,
|
||||||
|
&HashMap::from([(
|
||||||
|
WindowId(1),
|
||||||
|
SizeConstraints {
|
||||||
|
min_width: 500.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
);
|
||||||
|
let groups = group(&source, &constrained);
|
||||||
|
assert_eq!(groups[&WindowId(1)], groups[&WindowId(3)]);
|
||||||
|
assert_eq!(groups[&WindowId(2)], groups[&WindowId(4)]);
|
||||||
|
assert_ne!(
|
||||||
|
groups[&WindowId(1)],
|
||||||
|
groups[&WindowId(2)],
|
||||||
|
"fixed row boundary remains independent"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Maximum constraints clip actual presented bounds. A clipped
|
||||||
|
// leaf that does not move cannot hold the changing allocation.
|
||||||
|
let maximum = HashMap::from([(
|
||||||
|
WindowId(1),
|
||||||
|
SizeConstraints {
|
||||||
|
max_width: Some(100.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)]);
|
||||||
|
let clipped_source = solve(&tree, &maximum);
|
||||||
|
let mut wider = tree.clone();
|
||||||
|
wider.resize_window(WindowId(1), Direction::Right, 0.1).unwrap();
|
||||||
|
let clipped_target = solve(&wider, &maximum);
|
||||||
|
assert_eq!(clipped_source[&WindowId(1)], clipped_target[&WindowId(1)]);
|
||||||
|
assert!(!group(&clipped_source, &clipped_target).contains_key(&WindowId(1)));
|
||||||
|
|
||||||
|
// Retarget from the held presentation, not the previous logical
|
||||||
|
// destination. Reversing one branch leaves the other independent.
|
||||||
|
let mut reversed = target.clone();
|
||||||
|
for id in [WindowId(1), WindowId(2)] {
|
||||||
|
reversed.insert(id, source[&id]);
|
||||||
|
}
|
||||||
|
let groups = group(&source, &reversed);
|
||||||
|
assert!(!groups.contains_key(&WindowId(1)));
|
||||||
|
assert_eq!(groups[&WindowId(3)], groups[&WindowId(4)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tree_motion_ignores_settled_and_hidden_tiles_and_links_transitive_reflow() {
|
||||||
|
let ids = [WindowId(1), WindowId(2), WindowId(3), WindowId(4)];
|
||||||
|
let source = ids.map(|id| Rect::new((id.0 - 1) as f64 * 100.0, 0.0, 100.0, 100.0));
|
||||||
|
let target = [
|
||||||
|
Rect::new(0.0, 0.0, 140.0, 100.0),
|
||||||
|
Rect::new(140.0, 0.0, 100.0, 100.0),
|
||||||
|
Rect::new(240.0, 0.0, 60.0, 100.0),
|
||||||
|
source[3],
|
||||||
|
];
|
||||||
|
let groups = tree_reflow_groups(
|
||||||
|
ids.into_iter().enumerate().map(|(i, id)| (id, source[i], target[i])),
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
groups[&ids[0]], groups[&ids[2]],
|
||||||
|
"shared boundaries connect transitively"
|
||||||
|
);
|
||||||
|
assert!(!groups.contains_key(&ids[3]), "settled tile owns no reflow");
|
||||||
|
|
||||||
|
let mut tree = nested_tree();
|
||||||
|
tree.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||||
|
let before = tree.geometry(Rect::new(0.0, 0.0, 800.0, 600.0)).unwrap();
|
||||||
|
tree.resize_window(WindowId(2), Direction::Right, 0.1).unwrap();
|
||||||
|
let after = tree.geometry(Rect::new(0.0, 0.0, 800.0, 600.0)).unwrap();
|
||||||
|
let groups = tree_reflow_groups(
|
||||||
|
before
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(&id, &rect)| Some((id, rect, *after.get(&id)?))),
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!groups.contains_key(&WindowId(1)),
|
||||||
|
"inactive tab has no presented geometry"
|
||||||
|
);
|
||||||
|
assert_eq!(groups[&WindowId(2)], groups[&WindowId(3)]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn viewport_recipe_handles_cancellation_joint_pages_and_bounded_bursts() {
|
fn viewport_recipe_handles_cancellation_joint_pages_and_bounded_bursts() {
|
||||||
use ferese_layout::{ColumnWidth, ScrollingLayout};
|
use ferese_layout::{ColumnWidth, ScrollingLayout};
|
||||||
|
|||||||
@@ -31,6 +31,123 @@ fn fixture() -> (smithay::reexports::calloop::EventLoop<'static, Ferese>, Ferese
|
|||||||
(events, state, output)
|
(events, state, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tree_resize_does_not_hold_motion_in_an_unchanged_neighboring_branch() {
|
||||||
|
if !crate::startup_tests::private_runtime(
|
||||||
|
"state::window_lifecycle_tests::tree_resize_does_not_hold_motion_in_an_unchanged_neighboring_branch",
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (mut events, mut state, output) = fixture();
|
||||||
|
state.animation_test_time = Some(Duration::ZERO);
|
||||||
|
state.gap_config = GapConfig {
|
||||||
|
inner: 10.0,
|
||||||
|
outer: 10.0,
|
||||||
|
smart: false,
|
||||||
|
};
|
||||||
|
let mut clients = (0..4)
|
||||||
|
.map(|_| window(&mut state, &mut events, 0xff112233))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let ids = clients
|
||||||
|
.iter()
|
||||||
|
.map(|(window, _)| state.windows.ids()[window])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let [a, b, c, d] = ids.as_slice() else { unreachable!() };
|
||||||
|
let (a, b, c, d) = (*a, *b, *c, *d);
|
||||||
|
let mut tree = ferese_layout::LayoutTree::default();
|
||||||
|
tree.insert(a, None, Axis::Horizontal, 0.5).unwrap();
|
||||||
|
tree.insert(c, Some(a), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
tree.insert(b, Some(a), Axis::Vertical, 0.5).unwrap();
|
||||||
|
tree.insert(d, Some(c), Axis::Vertical, 0.5).unwrap();
|
||||||
|
state.workspaces.active_mut().layout = WorkspaceLayout::Tree(tree);
|
||||||
|
state.relayout();
|
||||||
|
state.animations_enabled = true;
|
||||||
|
let resize = |state: &mut Ferese, id| {
|
||||||
|
let WorkspaceLayout::Tree(tree) = &mut state.workspaces.active_mut().layout else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
tree.resize_window(id, Direction::Down, 0.1).unwrap();
|
||||||
|
state.relayout();
|
||||||
|
state.display_handle.flush_clients().unwrap();
|
||||||
|
};
|
||||||
|
resize(&mut state, c);
|
||||||
|
for (_, wire) in &mut clients[2..] {
|
||||||
|
ack_configure(wire, 3);
|
||||||
|
request(wire, 2, 1, &[6, 0, 0], None);
|
||||||
|
request(wire, 2, 6, &[], None);
|
||||||
|
}
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
for id in [c, d] {
|
||||||
|
assert!(state.windows.transaction(&id).is_none());
|
||||||
|
}
|
||||||
|
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(16));
|
||||||
|
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(32));
|
||||||
|
let right_targets = [c, d].map(|id| state.windows.geometry(&id).unwrap().logical);
|
||||||
|
resize(&mut state, a);
|
||||||
|
assert_eq!(
|
||||||
|
right_targets,
|
||||||
|
[c, d].map(|id| state.windows.geometry(&id).unwrap().logical)
|
||||||
|
);
|
||||||
|
assert!(state.windows.transaction(&a).is_some());
|
||||||
|
let before = [a, b, c, d].map(|id| state.windows.geometry(&id).unwrap().visual.current);
|
||||||
|
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(48));
|
||||||
|
let after = [a, b, c, d].map(|id| state.windows.geometry(&id).unwrap().visual.current);
|
||||||
|
assert_eq!(before[..2], after[..2], "linked left reflow must wait");
|
||||||
|
assert_ne!(before[2], after[2], "right branch must continue its existing motion");
|
||||||
|
assert_ne!(before[3], after[3]);
|
||||||
|
let frame = state.sample_frame(&output, Duration::from_millis(16));
|
||||||
|
assert_eq!(frame.windows[&a].geometry.visual.current, after[0]);
|
||||||
|
assert_ne!(frame.windows[&c].geometry.visual.current, after[2]);
|
||||||
|
for id in [a, c] {
|
||||||
|
let window = state.windows.window(id).unwrap();
|
||||||
|
let geometry = state.windows.geometry(&id).unwrap();
|
||||||
|
let location = state.space.element_location(window).unwrap();
|
||||||
|
assert_eq!(location.x, geometry.visual.current.x.round() as i32);
|
||||||
|
assert_eq!(location.y, geometry.visual.current.y.round() as i32);
|
||||||
|
let point = (geometry.visual.current.x + 5.0, geometry.visual.current.y + 5.0).into();
|
||||||
|
let (surface, origin) = state.window_surface_under(point).unwrap();
|
||||||
|
assert_eq!(surface, *window.toplevel().unwrap().wl_surface());
|
||||||
|
assert!((origin.y - geometry.visual.current.y).abs() < 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acknowledgement does not release the left branch; one committing client
|
||||||
|
// cannot release the other tile's linked resize boundary either.
|
||||||
|
ack_configure(&mut clients[0].1, 3);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
assert!(state.windows.transaction(&a).is_some());
|
||||||
|
request(&mut clients[0].1, 2, 1, &[6, 0, 0], None);
|
||||||
|
request(&mut clients[0].1, 2, 6, &[], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
assert!(state.windows.transaction(&a).is_none());
|
||||||
|
assert!(state.presentation_dependencies.reflow_blocked(a, &state.windows));
|
||||||
|
assert!(state.presentation_dependencies.reflow_blocked(b, &state.windows));
|
||||||
|
state.advance_animations_at(Duration::from_millis(284), Duration::from_millis(332));
|
||||||
|
assert!(state.windows.transaction(&b).is_none());
|
||||||
|
assert_eq!(state.windows.geometry(&a).unwrap().visual.current, after[0]);
|
||||||
|
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(348));
|
||||||
|
assert_ne!(state.windows.geometry(&a).unwrap().visual.current, after[0]);
|
||||||
|
|
||||||
|
// Removing the serial owner retires its wait, and switching layouts never
|
||||||
|
// carries a tree cohort into the new scrolling coordinate system.
|
||||||
|
resize(&mut state, a);
|
||||||
|
request(&mut clients[0].1, 2, 1, &[0, 0, 0], None);
|
||||||
|
request(&mut clients[0].1, 2, 6, &[], None);
|
||||||
|
dispatch(&mut events, &mut state);
|
||||||
|
assert!(!state.presentation_dependencies.reflow_blocked(c, &state.windows));
|
||||||
|
state
|
||||||
|
.workspaces
|
||||||
|
.set_active_layout_mode(ferese_core::LayoutMode::Scrolling, Rect::new(0.0, 0.0, 800.0, 600.0))
|
||||||
|
.unwrap();
|
||||||
|
state.relayout();
|
||||||
|
for id in [c, d] {
|
||||||
|
state.windows.clear_transaction(&id);
|
||||||
|
}
|
||||||
|
state.release_presentation_dependencies();
|
||||||
|
for id in [c, d] {
|
||||||
|
assert!(!state.presentation_dependencies.reflow_blocked(id, &state.windows));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn width_cycle_keeps_stacked_rows_together_through_holds_forecasts_and_reversal() {
|
fn width_cycle_keeps_stacked_rows_together_through_holds_forecasts_and_reversal() {
|
||||||
if !crate::startup_tests::private_runtime(
|
if !crate::startup_tests::private_runtime(
|
||||||
|
|||||||
Reference in new issue
Block a user