layer-shika/crates/adapters/src/wayland/surfaces/popup_surface.rs

302 lines
11 KiB
Rust

use layer_shika_domain::dimensions::{LogicalRect, LogicalSize as DomainLogicalSize};
use layer_shika_domain::value_objects::popup_behavior::ConstraintAdjustment as DomainConstraintAdjustment;
use layer_shika_domain::value_objects::popup_position::{Alignment, AnchorPoint, PopupPosition};
use log::info;
use slint::PhysicalSize;
use smithay_client_toolkit::reexports::protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1;
use std::rc::Rc;
use wayland_client::{
protocol::{wl_compositor::WlCompositor, wl_seat::WlSeat, wl_surface::WlSurface},
QueueHandle,
};
use wayland_protocols::wp::fractional_scale::v1::client::{
wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1,
wp_fractional_scale_v1::WpFractionalScaleV1,
};
use wayland_protocols::wp::viewporter::client::{
wp_viewport::WpViewport, wp_viewporter::WpViewporter,
};
use wayland_protocols::xdg::shell::client::{
xdg_popup::XdgPopup,
xdg_positioner::{Anchor, ConstraintAdjustment, Gravity, XdgPositioner},
xdg_surface::XdgSurface,
xdg_wm_base::XdgWmBase,
};
use super::app_state::AppState;
pub struct PopupSurfaceParams<'a> {
pub compositor: &'a WlCompositor,
pub xdg_wm_base: &'a XdgWmBase,
pub parent_layer_surface: &'a ZwlrLayerSurfaceV1,
pub fractional_scale_manager: Option<&'a WpFractionalScaleManagerV1>,
pub viewporter: Option<&'a WpViewporter>,
pub queue_handle: &'a QueueHandle<AppState>,
pub position: PopupPosition,
pub output_bounds: DomainLogicalSize,
pub constraint_adjustment: DomainConstraintAdjustment,
pub physical_size: PhysicalSize,
pub scale_factor: f32,
}
pub struct PopupSurface {
pub surface: Rc<WlSurface>,
pub xdg_surface: Rc<XdgSurface>,
pub xdg_popup: Rc<XdgPopup>,
pub fractional_scale: Option<Rc<WpFractionalScaleV1>>,
pub viewport: Option<Rc<WpViewport>>,
position: PopupPosition,
output_bounds: DomainLogicalSize,
constraint_adjustment: DomainConstraintAdjustment,
xdg_wm_base: Rc<XdgWmBase>,
queue_handle: QueueHandle<AppState>,
}
impl PopupSurface {
pub fn create(params: &PopupSurfaceParams<'_>) -> Self {
let surface = Rc::new(params.compositor.create_surface(params.queue_handle, ()));
let xdg_surface = Rc::new(params.xdg_wm_base.get_xdg_surface(
&surface,
params.queue_handle,
(),
));
let positioner = Self::create_positioner(params);
let xdg_popup = Rc::new(xdg_surface.get_popup(None, &positioner, params.queue_handle, ()));
info!("Attaching popup to layer surface via get_popup");
params.parent_layer_surface.get_popup(&xdg_popup);
let fractional_scale = params.fractional_scale_manager.map(|manager| {
info!("Creating fractional scale object for popup surface");
Rc::new(manager.get_fractional_scale(&surface, params.queue_handle, ()))
});
let viewport = params.viewporter.map(|vp| {
info!("Creating viewport for popup surface");
Rc::new(vp.get_viewport(&surface, params.queue_handle, ()))
});
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_possible_truncation)]
if let Some(ref vp) = viewport {
let logical_width = (params.physical_size.width as f32 / params.scale_factor) as i32;
let logical_height = (params.physical_size.height as f32 / params.scale_factor) as i32;
info!(
"Setting viewport destination to logical size: {}x{} (physical: {}x{}, scale: {})",
logical_width,
logical_height,
params.physical_size.width,
params.physical_size.height,
params.scale_factor
);
vp.set_destination(logical_width, logical_height);
}
surface.set_buffer_scale(1);
Self {
surface,
xdg_surface,
xdg_popup,
fractional_scale,
viewport,
position: params.position.clone(),
output_bounds: params.output_bounds,
constraint_adjustment: params.constraint_adjustment,
xdg_wm_base: Rc::new(params.xdg_wm_base.clone()),
queue_handle: params.queue_handle.clone(),
}
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_precision_loss)]
fn create_positioner(params: &PopupSurfaceParams<'_>) -> XdgPositioner {
let positioner = params
.xdg_wm_base
.create_positioner(params.queue_handle, ());
let logical_width = params.physical_size.width as f32 / params.scale_factor;
let logical_height = params.physical_size.height as f32 / params.scale_factor;
let (calculated_x, calculated_y) = compute_top_left(
&params.position,
DomainLogicalSize::from_raw(logical_width, logical_height),
params.output_bounds,
);
info!(
"Popup positioning: position={:?}, calculated_top_left=({}, {})",
params.position, calculated_x, calculated_y
);
let logical_width_i32 = logical_width as i32;
let logical_height_i32 = logical_height as i32;
positioner.set_anchor_rect(calculated_x, calculated_y, 1, 1);
positioner.set_size(logical_width_i32, logical_height_i32);
positioner.set_anchor(Anchor::TopLeft);
positioner.set_gravity(Gravity::BottomRight);
positioner
.set_constraint_adjustment(map_constraint_adjustment(params.constraint_adjustment));
positioner
}
pub fn grab(&self, seat: &WlSeat, serial: u32) {
info!("Grabbing popup with serial {serial}");
self.xdg_popup.grab(seat, serial);
info!("Committing popup surface to trigger configure event");
self.surface.commit();
}
pub fn update_viewport_size(&self, logical_width: i32, logical_height: i32) {
if let Some(ref vp) = self.viewport {
log::debug!(
"Updating popup viewport destination to logical size: {}x{}",
logical_width,
logical_height
);
vp.set_destination(logical_width, logical_height);
self.reposition_popup(logical_width, logical_height);
}
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_precision_loss)]
fn reposition_popup(&self, logical_width: i32, logical_height: i32) {
let (calculated_x, calculated_y) = compute_top_left(
&self.position,
DomainLogicalSize::from_raw(logical_width as f32, logical_height as f32),
self.output_bounds,
);
info!(
"Repositioning popup: new_size=({}x{}), new_top_left=({}, {})",
logical_width, logical_height, calculated_x, calculated_y
);
let positioner = self.xdg_wm_base.create_positioner(&self.queue_handle, ());
positioner.set_anchor_rect(calculated_x, calculated_y, 1, 1);
positioner.set_size(logical_width, logical_height);
positioner.set_anchor(Anchor::TopLeft);
positioner.set_gravity(Gravity::BottomRight);
positioner.set_constraint_adjustment(map_constraint_adjustment(self.constraint_adjustment));
self.xdg_popup.reposition(&positioner, 0);
}
pub fn destroy(&self) {
info!("Destroying popup surface");
self.xdg_popup.destroy();
self.xdg_surface.destroy();
self.surface.destroy();
}
}
fn compute_top_left(
position: &PopupPosition,
popup_size: DomainLogicalSize,
output_bounds: DomainLogicalSize,
) -> (i32, i32) {
let (mut x, mut y) = match position {
PopupPosition::Absolute { x, y } => (*x, *y),
PopupPosition::Centered { offset } => (
(output_bounds.width() / 2.0) - (popup_size.width() / 2.0) + offset.x,
(output_bounds.height() / 2.0) - (popup_size.height() / 2.0) + offset.y,
),
PopupPosition::Element {
rect,
anchor,
alignment,
} => {
let (anchor_x, anchor_y) = anchor_point_in_rect(*rect, *anchor);
let (ax, ay) = alignment_offsets(*alignment, popup_size);
(anchor_x - ax, anchor_y - ay)
}
PopupPosition::Cursor { .. } | PopupPosition::RelativeToParent { .. } => {
log::warn!("PopupPosition variant not supported by current backend: {position:?}");
(0.0, 0.0)
}
};
let max_x = (output_bounds.width() - popup_size.width()).max(0.0);
let max_y = (output_bounds.height() - popup_size.height()).max(0.0);
x = x.clamp(0.0, max_x);
y = y.clamp(0.0, max_y);
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_possible_wrap)]
(x as i32, y as i32)
}
fn anchor_point_in_rect(rect: LogicalRect, anchor: AnchorPoint) -> (f32, f32) {
let x = rect.x();
let y = rect.y();
let w = rect.width();
let h = rect.height();
match anchor {
AnchorPoint::TopLeft => (x, y),
AnchorPoint::TopCenter => (x + w / 2.0, y),
AnchorPoint::TopRight => (x + w, y),
AnchorPoint::CenterLeft => (x, y + h / 2.0),
AnchorPoint::Center => (x + w / 2.0, y + h / 2.0),
AnchorPoint::CenterRight => (x + w, y + h / 2.0),
AnchorPoint::BottomLeft => (x, y + h),
AnchorPoint::BottomCenter => (x + w / 2.0, y + h),
AnchorPoint::BottomRight => (x + w, y + h),
}
}
fn alignment_offsets(alignment: Alignment, popup_size: DomainLogicalSize) -> (f32, f32) {
let x = match alignment {
Alignment::Start => 0.0,
Alignment::Center => popup_size.width() / 2.0,
Alignment::End => popup_size.width(),
};
let y = match alignment {
Alignment::Start => 0.0,
Alignment::Center => popup_size.height() / 2.0,
Alignment::End => popup_size.height(),
};
(x, y)
}
fn map_constraint_adjustment(adjustment: DomainConstraintAdjustment) -> ConstraintAdjustment {
match adjustment {
DomainConstraintAdjustment::None => ConstraintAdjustment::None,
DomainConstraintAdjustment::Slide => {
ConstraintAdjustment::SlideX | ConstraintAdjustment::SlideY
}
DomainConstraintAdjustment::Flip => {
ConstraintAdjustment::FlipX | ConstraintAdjustment::FlipY
}
DomainConstraintAdjustment::Resize => {
ConstraintAdjustment::ResizeX | ConstraintAdjustment::ResizeY
}
DomainConstraintAdjustment::SlideAndResize => {
ConstraintAdjustment::SlideX
| ConstraintAdjustment::SlideY
| ConstraintAdjustment::ResizeX
| ConstraintAdjustment::ResizeY
}
DomainConstraintAdjustment::FlipAndSlide => {
ConstraintAdjustment::FlipX
| ConstraintAdjustment::FlipY
| ConstraintAdjustment::SlideX
| ConstraintAdjustment::SlideY
}
DomainConstraintAdjustment::All => ConstraintAdjustment::all(),
}
}