blob: 51db6c7cfc9435f9d153aecc2cc7f5f8f98aad95 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/gfx/x/x11_crtc_resizer.h"
#include <algorithm>
#include <iterator>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/stl_util.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/x/connection.h"
#include "ui/gfx/x/future.h"
namespace {
constexpr auto kInvalidMode = static_cast<x11::RandR::Mode>(0);
constexpr auto kDisabledCrtc = static_cast<x11::RandR::Crtc>(0);
// TODO(jamiewalch): Use the correct DPI for the mode: http://crbug.com/172405.
const int kDefaultDPI = 96;
int PixelsToMillimeters(int pixels, int dpi) {
DCHECK(dpi != 0);
const double kMillimetersPerInch = 25.4;
// (pixels / dpi) is the length in inches. Multiplying by
// kMillimetersPerInch converts to mm. Multiplication is done first to
// avoid integer division.
return static_cast<int>(kMillimetersPerInch * pixels / dpi);
}
} // namespace
namespace x11 {
X11CrtcResizer::CrtcInfo::CrtcInfo() = default;
X11CrtcResizer::CrtcInfo::CrtcInfo(
x11::RandR::Crtc crtc,
int16_t x,
int16_t y,
uint16_t width,
uint16_t height,
x11::RandR::Mode mode,
x11::RandR::Rotation rotation,
const std::vector<x11::RandR::Output>& outputs)
: crtc(crtc),
old_x(x),
x(x),
old_y(y),
y(y),
width(width),
height(height),
mode(mode),
rotation(rotation),
outputs(outputs) {}
X11CrtcResizer::CrtcInfo::CrtcInfo(const X11CrtcResizer::CrtcInfo&) = default;
X11CrtcResizer::CrtcInfo::CrtcInfo(X11CrtcResizer::CrtcInfo&&) = default;
X11CrtcResizer::CrtcInfo& X11CrtcResizer::CrtcInfo::operator=(
const X11CrtcResizer::CrtcInfo&) = default;
X11CrtcResizer::CrtcInfo& X11CrtcResizer::CrtcInfo::operator=(
X11CrtcResizer::CrtcInfo&&) = default;
X11CrtcResizer::CrtcInfo::~CrtcInfo() = default;
bool X11CrtcResizer::CrtcInfo::OffsetsChanged() const {
return old_x != x || old_y != y;
}
X11CrtcResizer::X11CrtcResizer(
x11::RandR::GetScreenResourcesCurrentReply* resources,
x11::Connection* connection)
: connection_(connection) {
if (resources != nullptr) {
resources_ = *resources;
}
// Unittests provide nullptr, and do not exercise code-paths that talk to the
// X server.
if (connection_) {
randr_ = &connection_->randr();
auto atom_reply = connection_->InternAtom({false, "WM_STATE"}).Sync();
if (atom_reply) {
wm_state_atom_ = atom_reply->atom;
} else {
LOG(ERROR) << "Failed to intern atom for WM_STATE.";
}
}
}
X11CrtcResizer::~X11CrtcResizer() = default;
void X11CrtcResizer::FetchActiveCrtcs() {
active_crtcs_.clear();
x11::Time config_timestamp = resources_.config_timestamp;
for (const auto& crtc : resources_.crtcs) {
auto response = randr_->GetCrtcInfo({crtc, config_timestamp}).Sync();
if (!response) {
continue;
}
if (response->outputs.empty()) {
continue;
}
AddCrtcFromReply(crtc, *response.reply);
}
}
x11::RandR::Crtc X11CrtcResizer::GetCrtcForOutput(
x11::RandR::Output output) const {
// This implementation assumes an output is attached to only one CRTC. If
// there are multiple CRTCs for the output, only the first will be returned,
// but this should never occur with Xorg+video-dummy.
auto iter =
std::ranges::find_if(active_crtcs_, [output](const CrtcInfo& crtc_info) {
return base::Contains(crtc_info.outputs, output);
});
if (iter == active_crtcs_.end()) {
return kDisabledCrtc;
}
return iter->crtc;
}
void X11CrtcResizer::DisableCrtc(x11::RandR::Crtc crtc) {
x11::Time config_timestamp = resources_.config_timestamp;
randr_->SetCrtcConfig({
.crtc = crtc,
.timestamp = x11::Time::CurrentTime,
.config_timestamp = config_timestamp,
.x = 0,
.y = 0,
.mode = kInvalidMode,
.rotation = x11::RandR::Rotation::Rotate_0,
.outputs = {},
});
}
void X11CrtcResizer::UpdateActiveCrtcs(x11::RandR::Crtc crtc,
x11::RandR::Mode mode,
const gfx::Size& new_size) {
updated_crtcs_.insert(crtc);
// Find |crtc| in |active_crtcs_| and adjust its mode and size.
auto iter = std::ranges::find(active_crtcs_, crtc, &CrtcInfo::crtc);
// |crtc| was returned by GetCrtcForOutput() so it should definitely be in
// the list.
DCHECK(iter != active_crtcs_.end());
iter->mode = mode;
RelayoutCrtcs(iter->crtc, new_size);
NormalizeCrtcs();
}
void X11CrtcResizer::UpdateActiveCrtc(x11::RandR::Crtc crtc,
x11::RandR::Mode mode,
const gfx::Rect& new_rect) {
updated_crtcs_.insert(crtc);
// Find |crtc| in |active_crtcs_| and adjust its mode and size.
auto iter = std::ranges::find(active_crtcs_, crtc, &CrtcInfo::crtc);
// |crtc| was returned by GetCrtcForOutput() so it should definitely be in
// the list.
DCHECK(iter != active_crtcs_.end());
iter->mode = mode;
iter->x = new_rect.x();
iter->y = new_rect.y();
iter->width = new_rect.width();
iter->height = new_rect.height();
}
void X11CrtcResizer::AddActiveCrtc(
x11::RandR::Crtc crtc,
x11::RandR::Mode mode,
const std::vector<x11::RandR::Output>& outputs,
const gfx::Rect& new_rect) {
// |crtc| is not active so it must not be in |active_crtcs_|.
DCHECK(std::ranges::find(active_crtcs_, crtc, &CrtcInfo::crtc) ==
active_crtcs_.end());
active_crtcs_.emplace_back(crtc, new_rect.x(), new_rect.y(), new_rect.width(),
new_rect.height(), mode,
x11::RandR::Rotation::Rotate_0, outputs);
updated_crtcs_.insert(crtc);
}
void X11CrtcResizer::RemoveActiveCrtc(x11::RandR::Crtc crtc) {
auto to_remove = std::ranges::remove(active_crtcs_, crtc, &CrtcInfo::crtc);
DCHECK(!to_remove.empty());
active_crtcs_.erase(to_remove.begin(), to_remove.end());
}
void X11CrtcResizer::RelayoutCrtcs(x11::RandR::Crtc crtc_to_resize,
const gfx::Size& new_size) {
if (LayoutIsVertical()) {
PackVertically(new_size, crtc_to_resize);
} else {
PackHorizontally(new_size, crtc_to_resize);
}
}
void X11CrtcResizer::DisableChangedCrtcs() {
for (const auto& crtc_info : active_crtcs_) {
// Updated CRTCs are expected to be disabled by the caller.
if (crtc_info.OffsetsChanged() &&
!updated_crtcs_.contains(crtc_info.crtc)) {
DisableCrtc(crtc_info.crtc);
}
}
}
void X11CrtcResizer::NormalizeCrtcs() {
gfx::Rect bounding_box;
for (const auto& crtc : active_crtcs_) {
bounding_box.Union(gfx::Rect(crtc.x, crtc.y, crtc.width, crtc.height));
}
bounding_box_size_ = bounding_box.size();
gfx::Vector2d adjustment =
gfx::Vector2d(-bounding_box.origin().x(), -bounding_box.origin().y());
if (adjustment.IsZero()) {
return;
}
for (auto& crtc : active_crtcs_) {
crtc.x += adjustment.x();
crtc.y += adjustment.y();
}
}
void X11CrtcResizer::MoveApplicationWindows() {
if (!connection_) {
// connection_ is nullptr in unittests.
return;
}
// Only direct descendants of the root window should be moved. Child
// windows automatically track the location of their parents, and can only
// be moved within their parent window.
auto query_response =
connection_->QueryTree({connection_->default_root()}).Sync();
if (!query_response) {
return;
}
for (const auto& window : query_response->children) {
auto attributes_response =
connection_->GetWindowAttributes({window}).Sync();
if (!attributes_response) {
continue;
}
if (attributes_response->map_state != x11::MapState::Viewable) {
// Unmapped or hidden windows can be left alone - their geometries
// might not be meaningful. If the window later becomes mapped, the
// window-manager will be responsible for its placement.
continue;
}
auto geometry_response = connection_->GetGeometry(window).Sync();
if (!geometry_response) {
continue;
}
// Look for any CRTC which contains the window's top-left corner. If the
// CRTC is being moved, request the window to be moved the same amount.
for (const auto& crtc_info : active_crtcs_) {
if (!crtc_info.OffsetsChanged()) {
continue;
}
auto old_rect = gfx::Rect(crtc_info.old_x, crtc_info.old_y,
crtc_info.width, crtc_info.height);
gfx::Vector2d window_top_left(geometry_response->x, geometry_response->y);
if (!old_rect.Contains(
gfx::Point(window_top_left.x(), window_top_left.y()))) {
continue;
}
gfx::Vector2d adjustment(crtc_info.x - crtc_info.old_x,
crtc_info.y - crtc_info.old_y);
window_top_left = window_top_left + adjustment;
MoveWindow(window, *attributes_response.reply,
gfx::Point(window_top_left.x(), window_top_left.y()));
break;
}
}
}
gfx::Size X11CrtcResizer::GetBoundingBox() const {
DCHECK(!bounding_box_size_.IsEmpty());
return bounding_box_size_;
}
void X11CrtcResizer::ApplyActiveCrtcs() {
for (const auto& crtc_info : active_crtcs_) {
if (crtc_info.OffsetsChanged() || updated_crtcs_.contains(crtc_info.crtc)) {
x11::Time config_timestamp = resources_.config_timestamp;
randr_->SetCrtcConfig({
.crtc = crtc_info.crtc,
.timestamp = x11::Time::CurrentTime,
.config_timestamp = config_timestamp,
.x = crtc_info.x,
.y = crtc_info.y,
.mode = crtc_info.mode,
.rotation = crtc_info.rotation,
.outputs = crtc_info.outputs,
});
}
}
updated_crtcs_.clear();
}
void X11CrtcResizer::UpdateRootWindow(x11::Window root) {
// Disable any CRTCs that have been changed, so that the root window can be
// safely resized to the bounding-box of the new CRTCs.
// This is non-optimal: the only CRTCs that need disabling are those whose
// original rectangles don't fit into the new root window - they are the ones
// that would prevent resizing the root window. But figuring these out would
// involve keeping track of all the original rectangles as well as the new
// ones. So, to keep the implementation simple (and working for any arbitrary
// layout algorithm), all changed CRTCs are disabled here.
DisableChangedCrtcs();
// Get the dimensions to resize the root window to.
auto dimensions = GetBoundingBox();
// TODO(lambroslambrou): Use the DPI from client size information.
uint32_t width_mm = PixelsToMillimeters(dimensions.width(), kDefaultDPI);
uint32_t height_mm = PixelsToMillimeters(dimensions.height(), kDefaultDPI);
randr_->SetScreenSize({root, static_cast<uint16_t>(dimensions.width()),
static_cast<uint16_t>(dimensions.height()), width_mm,
height_mm});
MoveApplicationWindows();
// Apply the new CRTCs, which will re-enable any that were disabled.
ApplyActiveCrtcs();
}
void X11CrtcResizer::SetCrtcsForTest(
std::vector<x11::RandR::GetCrtcInfoReply> crtcs) {
int id = 1;
for (const auto& crtc : crtcs) {
AddCrtcFromReply(static_cast<x11::RandR::Crtc>(id), crtc);
id++;
}
}
std::vector<gfx::Rect> X11CrtcResizer::GetCrtcsForTest() const {
std::vector<gfx::Rect> result;
for (const auto& crtc_info : active_crtcs_) {
result.emplace_back(crtc_info.x, crtc_info.y, crtc_info.width,
crtc_info.height);
}
return result;
}
void X11CrtcResizer::AddCrtcFromReply(
x11::RandR::Crtc crtc,
const x11::RandR::GetCrtcInfoReply& reply) {
active_crtcs_.emplace_back(crtc, reply.x, reply.y, reply.width, reply.height,
reply.mode, reply.rotation, reply.outputs);
}
bool X11CrtcResizer::LayoutIsVertical() const {
if (active_crtcs_.size() <= 1) {
return false;
}
// For simplicity, just pick 2 CRTCs arbitrarily.
auto iter1 = active_crtcs_.begin();
auto iter2 = std::next(iter1);
// The cases:
// --[---]--------
// --------[---]--
// and:
// --------[---]--
// --[---]--------
// are not vertically stacked. The case where the CRTCs are exactly touching
// is also not vertically stacked, because it comes from a horizontal
// packing of CRTCs:
// --[---]-------
// -------[---]--
// All other cases have overlapping projections so they are considered
// vertically stacked.
auto left1 = iter1->x;
auto right1 = iter1->x + iter1->width;
auto left2 = iter2->x;
auto right2 = iter2->x + iter2->width;
return right1 > left2 && right2 > left1;
}
void X11CrtcResizer::PackVertically(const gfx::Size& new_size,
x11::RandR::Crtc resized_crtc) {
// Before applying the new size, test for any current alignments to
// decide which alignment should be preserved, if any.
DCHECK(!active_crtcs_.empty());
bool is_left_aligned = true;
bool is_middle_aligned = true;
bool is_right_aligned = true;
auto first_crtc_left = active_crtcs_.front().x;
auto first_crtc_middle = first_crtc_left + active_crtcs_.front().width / 2;
auto first_crtc_right = first_crtc_left + active_crtcs_.front().width;
for (const auto& crtc_info : active_crtcs_) {
auto crtc_left = crtc_info.x;
auto crtc_middle = crtc_info.x + crtc_info.width / 2;
auto crtc_right = crtc_info.x + crtc_info.width;
if (crtc_left != first_crtc_left) {
is_left_aligned = false;
}
if (abs(crtc_middle - first_crtc_middle) > 1) {
is_middle_aligned = false;
}
if (crtc_right != first_crtc_right) {
is_right_aligned = false;
}
}
// Apply the new size.
for (auto& crtc_info : active_crtcs_) {
if (crtc_info.crtc == resized_crtc) {
crtc_info.width = new_size.width();
crtc_info.height = new_size.height();
break;
}
}
// Sort vertically before packing.
std::ranges::sort(active_crtcs_, std::less<>(), &CrtcInfo::y);
// Pack the CRTCs by setting their y-offsets. If necessary, change the
// x-offset for right-alignment.
int current_y = 0;
for (auto& crtc_info : active_crtcs_) {
crtc_info.y = current_y;
current_y += crtc_info.height;
// Place all monitors, respecting any alignment preference. If there are
// multiple possible alignments, prioritize left, then right, then middle.
// TODO(crbug.com/40225767): Implement a more sophisticated algorithm that
// tries to preserve pairwise alignment. It is not enough to leave the
// x-offsets unchanged here - this tends to result in the monitors being
// arranged roughly diagonally, wasting lots of space. Some amount of
// horizontal compression is needed to prevent this from happening.
if (is_left_aligned) {
crtc_info.x = 0;
} else if (is_right_aligned) {
crtc_info.x = -crtc_info.width;
} else if (is_middle_aligned) {
crtc_info.x = -crtc_info.width / 2;
} else {
// The current implementation left-aligns the CRTCs if no other
// alignment is detected.
// TODO(crbug.com/40225767): A future enhancement may be to detect and
// report one of {left, middle, right, none}. The "none" case (for
// vertical and horizontal layouts) could be treated as a
// client-controlled layout, where the host does not attempt any
// repositioning. In this case, the host could still support
// resize-to-fit, but in a simplified way - resize would be allowed
// whenever it creates no overlaps.
crtc_info.x = 0;
}
}
}
void X11CrtcResizer::PackHorizontally(const gfx::Size& new_size,
x11::RandR::Crtc resized_crtc) {
gfx::Size new_size_transposed(new_size.height(), new_size.width());
Transpose();
PackVertically(new_size_transposed, resized_crtc);
Transpose();
}
void X11CrtcResizer::Transpose() {
for (auto& crtc_info : active_crtcs_) {
std::swap(crtc_info.x, crtc_info.y);
std::swap(crtc_info.width, crtc_info.height);
}
}
void X11CrtcResizer::MoveWindow(x11::Window window,
const x11::GetWindowAttributesReply& attributes,
gfx::Point top_left) {
if (!attributes.override_redirect) {
// Try to locate the original window which was re-parented by the
// window-manager.
auto app_window = FindAppWindow(window, attributes);
if (app_window != x11::Window::None) {
window = app_window;
}
}
connection_->ConfigureWindow({
.window = window,
.x = top_left.x(),
.y = top_left.y(),
});
}
x11::Window X11CrtcResizer::FindAppWindow(
x11::Window window,
const x11::GetWindowAttributesReply& attributes) {
// Only descend into visible windows.
if (attributes.map_state != x11::MapState::Viewable) {
return x11::Window::None;
}
// If the window itself has the WM_STATE property, return it. Otherwise,
// descend into the window's children recursively.
auto property_reply = connection_
->GetProperty({
.window = window,
.property = wm_state_atom_,
.long_length = 1,
})
.Sync();
if (!property_reply) {
return x11::Window::None;
}
if (property_reply->value_len != 0) {
// Found window with WM_STATE property.
return window;
}
// Descend into the children of |window| in stacking order. See for example:
// Find_Client_In_Children() at
// https://github.com/freedesktop/xorg-xwininfo/blob/master/clientwin.c
auto query_response = connection_->QueryTree({window}).Sync();
if (!query_response) {
return x11::Window::None;
}
for (const auto& child_window : base::Reversed(query_response->children)) {
auto attributes_response =
connection_->GetWindowAttributes({child_window}).Sync();
if (!attributes_response) {
return x11::Window::None;
}
auto found_window = FindAppWindow(child_window, *attributes_response.reply);
if (found_window != x11::Window::None) {
return found_window;
}
}
return x11::Window::None;
}
} // namespace x11