Start simplifying popup logic

So uh when did Smithay have all these popup helpers, did I just not read the docs hard enough
This commit is contained in:
Ottatop 2024-04-11 14:49:30 -05:00
parent 220be6d505
commit 97efdb854c

View file

@ -12,7 +12,7 @@ use smithay::{
xdg_toplevel::{self, ResizeEdge},
},
wayland_server::{
protocol::{wl_output::WlOutput, wl_seat::WlSeat},
protocol::{wl_output::WlOutput, wl_seat::WlSeat, wl_surface::WlSurface},
Resource,
},
},
@ -99,512 +99,39 @@ impl XdgShellHandler for State {
}
// this is 500 lines there has to be a shorter way to do this
fn new_popup(&mut self, surface: PopupSurface, mut positioner: PositionerState) {
tracing::debug!(?positioner.constraint_adjustment, ?positioner.gravity);
let output_rect = self
.focused_output()
.and_then(|op| self.space.output_geometry(op));
fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) {
tracing::info!("XdgShellHandler::new_popup");
/// Horizontal direction
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DirH {
Left,
Right,
}
let popup_geo = (|| -> Option<Rectangle<i32, Logical>> {
let root = find_popup_root_surface(&PopupKind::Xdg(surface.clone())).ok()?;
let parent = surface.get_parent_surface()?;
/// Vertical direction
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DirV {
Top,
Bottom,
}
let mut is_subpopup = false;
// We have to go through this really verbose way of getting the location of the popup in
// the global space.
//
// The location from PopupSurface.with_pending_state's state.geometry.loc is relative to
// its parent. When its parent is a window, we can simply add the window's location to the
// popup's to get its global location.
//
// However, if its parent is another popup, we need to step upwards and grab the location
// from each popup, aggregating them into one global space location.
let global_loc = {
let mut surf = surface.get_parent_surface();
let mut loc = surface.with_pending_state(|state| state.geometry.loc);
tracing::debug!(?loc);
while let Some(s) = &surf {
if let Some(popup) = self.popup_manager.find_popup(s) {
// popup.geometry() doesn't return the right location, so we dive into the
// PopupSurface's state to grab it.
let PopupKind::Xdg(popup_surf) = &popup else { return }; // TODO:
let l = popup_surf.with_pending_state(|state| state.geometry.loc);
tracing::debug!(loc = ?l, "parent is popup");
loc += l;
is_subpopup = true;
surf = popup_surf.get_parent_surface();
} else if let Some(win) = self.window_for_surface(s) {
// Once we reach a window, we can stop as windows are already in the global space.
tracing::debug!("parent is window");
loc += self
.space
.element_location(&win)
.unwrap_or_else(|| (0, 0).into());
tracing::debug!(?loc);
break;
} else {
// Or a layer
let layer_and_op = self.space.outputs().find_map(|op| {
let layer_map = layer_map_for_output(op);
let ret = layer_map
.layers()
.find(|l| l.wl_surface() == s)
.cloned()
.map(|layer| {
(
layer_map.layer_geometry(&layer).unwrap_or_default(),
op.clone(),
)
});
ret
});
if let Some((layer_geo, op)) = layer_and_op {
let op_loc = self.space.output_geometry(&op).unwrap_or_default().loc;
loc += layer_geo.loc + op_loc;
break;
let win = self.window_for_surface(&root)?;
let win_loc = self.space.element_geometry(&win)?.loc;
let parent_loc = if root == parent {
win_loc
} else {
match self.popup_manager.find_popup(&parent)? {
PopupKind::Xdg(surf) => {
surf.with_pending_state(|state| state.geometry.loc) + win_loc
}
}
}
loc
};
let mut popup_rect_global = {
let mut rect = positioner.get_geometry();
rect.loc = global_loc;
rect
};
tracing::debug!(?global_loc);
// The final rectangle that the popup is set to needs to be relative to its parent,
// so we store its old location here and subtract it at the end.
let loc_diff = global_loc - surface.with_pending_state(|state| state.geometry.loc);
if let Some(output_rect) = output_rect {
// Check if the rect is constrained horizontally, and if so, which side.
let constrained_x = |rect: Rectangle<i32, Logical>| -> Option<DirH> {
tracing::debug!(?rect, ?output_rect);
if rect.loc.x < output_rect.loc.x {
Some(DirH::Left)
} else if rect.loc.x + rect.size.w > output_rect.loc.x + output_rect.size.w {
Some(DirH::Right)
} else {
None
PopupKind::InputMethod(_) => return None,
}
};
// Check if the rect is constrained vertically, and if so, which side.
let constrained_y = |rect: Rectangle<i32, Logical>| -> Option<DirV> {
tracing::debug!(?rect, ?output_rect);
let mut output_geo = win
.output(self)
.and_then(|op| self.space.output_geometry(&op))?;
if rect.loc.y < output_rect.loc.y {
Some(DirV::Top)
} else if rect.loc.y + rect.size.h > output_rect.loc.y + output_rect.size.h {
Some(DirV::Bottom)
} else {
None
}
};
// Make local to parent
output_geo.loc -= dbg!(parent_loc);
Some(positioner.get_unconstrained_geometry(output_geo))
})()
.unwrap_or_else(|| positioner.get_geometry());
// We're going to need to position popups such that they stay fully onscreen.
// We can use the provided `positioner.constraint_adjustment` to get hints on how
// the popups want to be relocated.
dbg!(popup_geo);
let output_left_x = output_rect.loc.x;
let output_right_x = output_rect.loc.x + output_rect.size.w;
let output_top_y = output_rect.loc.y;
let output_bottom_y = output_rect.loc.y + output_rect.size.h;
// The popup is flowing offscreen in the horizontal direction.
if let Some(constrain_dir) = constrained_x(popup_rect_global) {
tracing::debug!("Popup was constrained on the x axis, repositioning");
let gravity = match positioner.gravity {
Gravity::Left | Gravity::TopLeft | Gravity::BottomLeft => DirH::Left,
_ => DirH::Right,
};
tracing::debug!(?gravity);
tracing::debug!(?constrain_dir);
'block: {
// If the constraint_adjustment has SlideX, we attempt to slide the popup
// towards the direction specified by positioner.gravity until TODO:
if positioner
.constraint_adjustment
.contains(ConstraintAdjustment::SlideX)
&& !is_subpopup
// If it's a subpopup, flip instead of slide. This makes
// stuff like Firefox nested dropdowns more intuitive.
{
tracing::debug!("Attempting to slide popup X");
// Slide towards the gravity until the opposite edge is unconstrained or the
// same edge is constrained
match (&constrain_dir, &gravity) {
(DirH::Left, DirH::Right) => {
let len_until_same_edge_constrained = output_right_x
- (popup_rect_global.loc.x + popup_rect_global.size.w);
let len_until_opp_edge_unconstrained =
output_left_x - popup_rect_global.loc.x;
popup_rect_global.loc.x += i32::min(
len_until_same_edge_constrained,
len_until_opp_edge_unconstrained,
)
.max(0);
tracing::debug!(
?popup_rect_global,
"Constrained SlideX left right"
);
}
(DirH::Right, DirH::Left) => {
let len_until_same_edge_constrained =
popup_rect_global.loc.x - output_left_x;
let len_until_opp_edge_unconstrained = (popup_rect_global.loc.x
+ popup_rect_global.size.w)
- output_right_x;
popup_rect_global.loc.x -= i32::min(
len_until_opp_edge_unconstrained,
len_until_same_edge_constrained,
)
.max(0);
tracing::debug!(
?popup_rect_global,
"Constrained SlideX right left"
);
}
_ => (),
};
if constrained_x(popup_rect_global).is_none() {
break 'block;
}
// Do the same but in the other direction
match (constrain_dir, gravity) {
(DirH::Right, DirH::Right) => {
let len_until_same_edge_unconstrained =
popup_rect_global.loc.x - output_left_x;
let len_until_opp_edge_constrained = (popup_rect_global.loc.x
+ popup_rect_global.size.w)
- output_right_x;
popup_rect_global.loc.x -= i32::min(
len_until_opp_edge_constrained,
len_until_same_edge_unconstrained,
)
.max(0);
tracing::debug!(
?popup_rect_global,
"Constrained SlideX right right"
);
}
(DirH::Left, DirH::Left) => {
let len_until_same_edge_unconstrained = output_right_x
- (popup_rect_global.loc.x + popup_rect_global.size.w);
let len_until_opp_edge_constrained =
output_left_x - popup_rect_global.loc.x;
popup_rect_global.loc.x += i32::min(
len_until_same_edge_unconstrained,
len_until_opp_edge_constrained,
)
.max(0);
tracing::debug!(?popup_rect_global, "Constrained SlideX left left");
}
_ => (),
};
if constrained_x(popup_rect_global).is_none() {
break 'block;
}
}
// If the above didn't bring the popup onscreen or if it's a nested popup, flip it.
if positioner
.constraint_adjustment
.contains(ConstraintAdjustment::FlipX)
{
tracing::debug!("Attempting to flip popup X");
let old_gravity = positioner.gravity;
positioner.gravity = match positioner.gravity {
Gravity::Left => Gravity::Right,
Gravity::Right => Gravity::Left,
Gravity::TopLeft => Gravity::TopRight,
Gravity::BottomLeft => Gravity::BottomRight,
Gravity::TopRight => Gravity::TopLeft,
Gravity::BottomRight => Gravity::BottomLeft,
rest => rest,
};
let old_anchor = positioner.anchor_edges;
positioner.anchor_edges = match positioner.anchor_edges {
Anchor::Left => Anchor::Right,
Anchor::Right => Anchor::Left,
Anchor::TopLeft => Anchor::TopRight,
Anchor::BottomLeft => Anchor::BottomRight,
Anchor::TopRight => Anchor::TopLeft,
Anchor::BottomRight => Anchor::BottomLeft,
rest => rest,
};
let mut relative_geo = positioner.get_geometry();
relative_geo.loc += loc_diff;
tracing::debug!(?relative_geo, "FlipX");
if constrained_x(relative_geo).is_none() {
popup_rect_global = relative_geo;
break 'block;
}
// The protocol states that if flipping it didn't bring it onscreen,
// then it should just stay at its unflipped state.
positioner.gravity = old_gravity;
positioner.anchor_edges = old_anchor;
}
// Finally, if flipping it failed, resize it to fit.
if positioner
.constraint_adjustment
.contains(ConstraintAdjustment::ResizeX)
{
tracing::debug!("Resizing popup X");
// Slice off the left side
if popup_rect_global.loc.x < output_left_x {
let len_to_slice = output_left_x - popup_rect_global.loc.x;
let new_top_left: Point<i32, Logical> = (
popup_rect_global.loc.x + len_to_slice,
popup_rect_global.loc.y,
)
.into();
let bottom_right: Point<i32, Logical> = (
popup_rect_global.loc.x + popup_rect_global.size.w,
popup_rect_global.loc.y + popup_rect_global.size.h,
)
.into();
popup_rect_global =
Rectangle::from_extemities(new_top_left, bottom_right);
}
// Slice off the right side
if popup_rect_global.loc.x + popup_rect_global.size.w > output_right_x {
let len_to_slice = (popup_rect_global.loc.x + popup_rect_global.size.w)
- output_right_x;
let top_left = popup_rect_global.loc;
let new_bottom_right: Point<i32, Logical> = (
popup_rect_global.loc.x + popup_rect_global.size.w - len_to_slice,
popup_rect_global.loc.y + popup_rect_global.size.h,
)
.into();
popup_rect_global =
Rectangle::from_extemities(top_left, new_bottom_right);
}
if constrained_x(popup_rect_global).is_none() {
break 'block;
}
}
}
}
// The popup is flowing offscreen in the vertical direction.
if let Some(constrain_dir) = constrained_y(popup_rect_global) {
tracing::debug!("Popup was constrained on the y axis, repositioning");
let gravity = match positioner.gravity {
Gravity::Top | Gravity::TopLeft | Gravity::TopRight => DirV::Top,
_ => DirV::Bottom,
};
tracing::debug!(?gravity);
tracing::debug!(?constrain_dir);
'block: {
// If the constraint_adjustment has SlideY, we attempt to slide the popup
// towards the direction specified by positioner.gravity until TODO:
if positioner
.constraint_adjustment
.contains(ConstraintAdjustment::SlideY)
&& !is_subpopup
// If it's a subpopup, flip instead of slide. This makes
// stuff like Firefox nested dropdowns more intuitive.
{
// Slide towards the gravity until the opposite edge is unconstrained or the
// same edge is constrained
match (&constrain_dir, &gravity) {
(DirV::Top, DirV::Bottom) => {
let len_until_same_edge_constrained = output_bottom_y
- (popup_rect_global.loc.y + popup_rect_global.size.h);
let len_until_opp_edge_unconstrained =
output_top_y - popup_rect_global.loc.y;
popup_rect_global.loc.y += i32::min(
len_until_same_edge_constrained,
len_until_opp_edge_unconstrained,
)
.max(0);
}
(DirV::Bottom, DirV::Top) => {
let len_until_same_edge_constrained =
popup_rect_global.loc.y - output_top_y;
let len_until_opp_edge_unconstrained = (popup_rect_global.loc.y
+ popup_rect_global.size.h)
- output_bottom_y;
popup_rect_global.loc.y -= i32::min(
len_until_opp_edge_unconstrained,
len_until_same_edge_constrained,
)
.max(0);
}
_ => (),
};
if constrained_y(popup_rect_global).is_none() {
break 'block;
}
// Do the same but in the other direction
match (constrain_dir, gravity) {
(DirV::Bottom, DirV::Bottom) => {
let len_until_same_edge_unconstrained =
popup_rect_global.loc.y - output_top_y;
let len_until_opp_edge_constrained = (popup_rect_global.loc.y
+ popup_rect_global.size.h)
- output_bottom_y;
popup_rect_global.loc.y -= i32::min(
len_until_opp_edge_constrained,
len_until_same_edge_unconstrained,
)
.max(0);
}
(DirV::Top, DirV::Top) => {
let len_until_same_edge_unconstrained = output_bottom_y
- (popup_rect_global.loc.y + popup_rect_global.size.h);
let len_until_opp_edge_constrained =
output_top_y - popup_rect_global.loc.y;
popup_rect_global.loc.y += i32::min(
len_until_same_edge_unconstrained,
len_until_opp_edge_constrained,
)
.max(0);
}
_ => (),
};
if constrained_y(popup_rect_global).is_none() {
break 'block;
}
}
// If the above didn't bring the popup onscreen or if it's a nested popup, flip it.
if positioner
.constraint_adjustment
.contains(ConstraintAdjustment::FlipY)
{
let old_gravity = positioner.gravity;
positioner.gravity = match positioner.gravity {
Gravity::Top => Gravity::Bottom,
Gravity::Bottom => Gravity::Top,
Gravity::TopLeft => Gravity::BottomLeft,
Gravity::BottomLeft => Gravity::TopLeft,
Gravity::TopRight => Gravity::BottomRight,
Gravity::BottomRight => Gravity::TopRight,
rest => rest,
};
let old_anchor = positioner.anchor_edges;
positioner.anchor_edges = match positioner.anchor_edges {
Anchor::Top => Anchor::Bottom,
Anchor::Bottom => Anchor::Top,
Anchor::TopLeft => Anchor::BottomLeft,
Anchor::BottomLeft => Anchor::TopLeft,
Anchor::TopRight => Anchor::BottomRight,
Anchor::BottomRight => Anchor::TopRight,
rest => rest,
};
let mut geo = positioner.get_geometry();
tracing::debug!(?geo, "Flipped Y geo");
geo.loc += loc_diff;
geo.loc.x = popup_rect_global.loc.x;
tracing::debug!(?geo, "Flipped Y geo global");
if constrained_y(geo).is_none() {
popup_rect_global = geo;
break 'block;
}
// The protocol states that if flipping it didn't bring it onscreen,
// then it should just stay at its unflipped state.
positioner.gravity = old_gravity;
positioner.anchor_edges = old_anchor;
}
// Finally, if flipping it failed, resize it to fit.
if positioner
.constraint_adjustment
.contains(ConstraintAdjustment::ResizeY)
{
// Slice off the top side
if popup_rect_global.loc.y < output_top_y {
let len_to_slice = output_top_y - popup_rect_global.loc.y;
let new_top_left: Point<i32, Logical> = (
popup_rect_global.loc.x,
popup_rect_global.loc.y + len_to_slice,
)
.into();
let bottom_right: Point<i32, Logical> = (
popup_rect_global.loc.x + popup_rect_global.size.w,
popup_rect_global.loc.y + popup_rect_global.size.h,
)
.into();
popup_rect_global =
Rectangle::from_extemities(new_top_left, bottom_right);
}
// Slice off the right side
if popup_rect_global.loc.y + popup_rect_global.size.h > output_bottom_y {
let len_to_slice = (popup_rect_global.loc.y + popup_rect_global.size.h)
- output_bottom_y;
let top_left = popup_rect_global.loc;
let new_bottom_right: Point<i32, Logical> = (
popup_rect_global.loc.x + popup_rect_global.size.w,
popup_rect_global.loc.y + popup_rect_global.size.h - len_to_slice,
)
.into();
popup_rect_global =
Rectangle::from_extemities(top_left, new_bottom_right);
}
if constrained_y(popup_rect_global).is_none() {
break 'block;
}
}
}
}
}
tracing::debug!(?popup_rect_global, "New popup");
popup_rect_global.loc -= loc_diff;
surface.with_pending_state(|state| state.geometry = popup_rect_global);
surface.with_pending_state(|state| state.geometry = popup_geo);
if let Err(err) = self.popup_manager.track_popup(PopupKind::from(surface)) {
tracing::warn!("failed to track popup: {}", err);