Popup window repositioning in weston

The problem

When removing a output in handling unplugging a HDMI display, weston will reposition all views into the first output if the views aren’t within the region of any outputs in shell_reposition_view_on_output_change() of desktop-shell/shell.c. And if a view is a popup, the geometry of that view is set to a relation position to its parent view here.

When the relative position of a popup view has negative values, the repositioning in aforementioned function will reckon this view invisible and move it to first quarter of the first output!

if (!visible) {
	first_output = container_of(ec->output_list.next,
				    struct weston_output, link);
	x = first_output->x + first_output->width / 4;
	y = first_output->y + first_output->height / 4;
	weston_view_set_position(view, x, y);
}

For example, in our use case, Brightness Bar is a popup view and its parent is System Controls. The following table shows the positions of these two views on an internal display of resolution 1920x1080 (first output):

Window Absolute Position Relative Position
System Controls (534,281) N/A
Brightness Bar (1045,135) (511,-146)

The position (511,-146) is out of the region of 1920x1080 in the checking of shell_reposition_view_on_output_change(). When a HDMI display is unplugged, the Brightness Bar will move to absolute position (480,270), which is different from its original absolute position (1045,135).

The fix

Since the position of popup view is relative to its parent, so when repositioning a popup view, we should convert it to the absolute position before checking if the position is within the region of any outputs.

In order to do the conversion, we should check if the role of the view is WESTON_DESKTOP_XDG_SURFACE_ROLE_POPUP, but the role information is only accessible in libweston-desktop/xdg-shell-v6.c. To keep it simple, we check if any coordinate is negative:

if (x < 0 || y < 0) {
	parent_view = view->geometry.parent;
	if (parent_view) {
		x += parent_view->geometry.pos_offset.x;
		y += parent_view->geometry.pos_offset.y;
	}
}

If both coordinates are positive, the check of within a output region is sufficient to make sure the view is visible.

visible = 0;
if (ec->clone_mode && ec->primary_output) {
	/* In clone mode, all outputs have the same geometry and region,
	 * so only need to check if the view is visible in primary output
	 */
	if (weston_output_contains_point(ec->primary_output, x,y)) {
		visible = 1;
	}
} else {
	wl_list_for_each(output, &ec->output_list, link) {
		if (weston_output_contains_point(output, x, y)) {
			visible = 1;
			break;
		}
	}
}

Side note

In wayland protocol, xdg_positioner.set_anchor_rect is used to implement the position of a popup surface:

    <request name="set_anchor_rect">
      <description summary="set the anchor rectangle within the parent surface">
	Specify the anchor rectangle within the parent surface that the child
	surface will be placed relative to. The rectangle is relative to the
	window geometry as defined by xdg_surface.set_window_geometry of the
	parent surface.

	When the xdg_positioner object is used to position a child surface, the
	anchor rectangle may not extend outside the window geometry of the
	positioned child's parent surface.

	If a negative size is set the invalid_input error is raised.
      </description>
      <arg name="x" type="int" summary="x position of anchor rectangle"/>
      <arg name="y" type="int" summary="y position of anchor rectangle"/>
      <arg name="width" type="int" summary="width of anchor rectangle"/>
      <arg name="height" type="int" summary="height of anchor rectangle"/>
    </request>

In Qt, it’s done in QWaylandXdgSurface::setPopup():

void QWaylandXdgSurface::setPopup(QWaylandWindow *parent)
{
    Q_ASSERT(!m_toplevel && !m_popup);

    auto positioner = new QtWayland::xdg_positioner(m_shell->m_xdgWmBase->create_positioner());
    // set_popup expects a position relative to the parent
    QRect windowGeometry = m_window->windowContentGeometry();
    QMargins windowMargins = m_window->windowContentMargins() - m_window->clientSideMargins();
    QMargins parentMargins = parent->windowContentMargins() - parent->clientSideMargins();
    QPoint transientPos = m_window->geometry().topLeft(); // this is absolute
    transientPos += QPoint(windowMargins.left(), windowMargins.top());
    transientPos -= parent->geometry().topLeft();
    transientPos -= QPoint(parentMargins.left(), parentMargins.top());
    positioner->set_anchor_rect(transientPos.x(), transientPos.y(), 1, 1);
    positioner->set_anchor(QtWayland::xdg_positioner::anchor_top_left);
    positioner->set_gravity(QtWayland::xdg_positioner::gravity_bottom_right);
    positioner->set_size(windowGeometry.width(), windowGeometry.height());
    positioner->set_constraint_adjustment(QtWayland::xdg_positioner::constraint_adjustment_slide_x
        | QtWayland::xdg_positioner::constraint_adjustment_slide_y);
    m_popup = new Popup(this, parent, positioner);
    positioner->destroy();

    delete positioner;
}

And there’s actually a commit in upstream qtwayland about invalid anchor rectangles. As the comment in that commit said, Qt’s own clients are supplying invalid anchor rectangles (and violating xdg_positioner protocol), so it’s just a warning but not an error.

Have fun!

comments powered by Disqus