1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
|
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/exo/wayland/wayland_positioner.h"
#include <numeric>
#include <ostream>
namespace exo::wayland {
namespace {
std::pair<WaylandPositioner::Direction, WaylandPositioner::Direction>
DecomposeAnchor(uint32_t anchor) {
switch (anchor) {
default:
case XDG_POSITIONER_ANCHOR_NONE:
return std::make_pair(WaylandPositioner::Direction::kNeutral,
WaylandPositioner::Direction::kNeutral);
case XDG_POSITIONER_ANCHOR_TOP:
return std::make_pair(WaylandPositioner::Direction::kNeutral,
WaylandPositioner::Direction::kNegative);
case XDG_POSITIONER_ANCHOR_BOTTOM:
return std::make_pair(WaylandPositioner::Direction::kNeutral,
WaylandPositioner::Direction::kPositive);
case XDG_POSITIONER_ANCHOR_LEFT:
return std::make_pair(WaylandPositioner::Direction::kNegative,
WaylandPositioner::Direction::kNeutral);
case XDG_POSITIONER_ANCHOR_RIGHT:
return std::make_pair(WaylandPositioner::Direction::kPositive,
WaylandPositioner::Direction::kNeutral);
case XDG_POSITIONER_ANCHOR_TOP_LEFT:
return std::make_pair(WaylandPositioner::Direction::kNegative,
WaylandPositioner::Direction::kNegative);
case XDG_POSITIONER_ANCHOR_BOTTOM_LEFT:
return std::make_pair(WaylandPositioner::Direction::kNegative,
WaylandPositioner::Direction::kPositive);
case XDG_POSITIONER_ANCHOR_TOP_RIGHT:
return std::make_pair(WaylandPositioner::Direction::kPositive,
WaylandPositioner::Direction::kNegative);
case XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT:
return std::make_pair(WaylandPositioner::Direction::kPositive,
WaylandPositioner::Direction::kPositive);
}
}
std::pair<WaylandPositioner::Direction, WaylandPositioner::Direction>
DecomposeGravity(uint32_t gravity) {
switch (gravity) {
default:
case XDG_POSITIONER_GRAVITY_NONE:
return std::make_pair(WaylandPositioner::Direction::kNeutral,
WaylandPositioner::Direction::kNeutral);
case XDG_POSITIONER_GRAVITY_TOP:
return std::make_pair(WaylandPositioner::Direction::kNeutral,
WaylandPositioner::Direction::kNegative);
case XDG_POSITIONER_GRAVITY_BOTTOM:
return std::make_pair(WaylandPositioner::Direction::kNeutral,
WaylandPositioner::Direction::kPositive);
case XDG_POSITIONER_GRAVITY_LEFT:
return std::make_pair(WaylandPositioner::Direction::kNegative,
WaylandPositioner::Direction::kNeutral);
case XDG_POSITIONER_GRAVITY_RIGHT:
return std::make_pair(WaylandPositioner::Direction::kPositive,
WaylandPositioner::Direction::kNeutral);
case XDG_POSITIONER_GRAVITY_TOP_LEFT:
return std::make_pair(WaylandPositioner::Direction::kNegative,
WaylandPositioner::Direction::kNegative);
case XDG_POSITIONER_GRAVITY_BOTTOM_LEFT:
return std::make_pair(WaylandPositioner::Direction::kNegative,
WaylandPositioner::Direction::kPositive);
case XDG_POSITIONER_GRAVITY_TOP_RIGHT:
return std::make_pair(WaylandPositioner::Direction::kPositive,
WaylandPositioner::Direction::kNegative);
case XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT:
return std::make_pair(WaylandPositioner::Direction::kPositive,
WaylandPositioner::Direction::kPositive);
}
}
static WaylandPositioner::Direction Flip(WaylandPositioner::Direction d) {
return (WaylandPositioner::Direction)-d;
}
// Represents the possible/actual positioner adjustments for this window.
struct ConstraintAdjustment {
bool flip;
bool slide;
bool resize;
bool allows_all() const { return flip && slide && resize; }
};
// Decodes an adjustment bit field into the structure.
ConstraintAdjustment MaskToConstraintAdjustment(uint32_t field,
uint32_t flip_mask,
uint32_t slide_mask,
uint32_t resize_mask) {
return {!!(field & flip_mask), !!(field & slide_mask),
!!(field & resize_mask)};
}
// A 1-dimensional projection of a range (a.k.a. a segment), used to solve the
// positioning problem in 1D.
struct Range1D {
int32_t start;
int32_t end;
Range1D GetTranspose(int32_t offset) const {
return {start + offset, end + offset};
}
int32_t center() const { return std::midpoint(start, end); }
};
// Works out the range's position that results from using exactly the
// adjustments specified by |adjustments|.
Range1D Calculate(const ConstraintAdjustment& adjustments,
int32_t work_size,
Range1D anchor_range,
uint32_t size,
int32_t offset,
WaylandPositioner::Direction anchor,
WaylandPositioner::Direction gravity) {
if (adjustments.flip) {
return Calculate({/*flip=*/false, adjustments.slide, adjustments.resize},
work_size, anchor_range, size, -offset, Flip(anchor),
Flip(gravity));
}
if (adjustments.resize) {
Range1D unresized =
Calculate({/*flip=*/false, adjustments.slide, /*resize=*/false},
work_size, anchor_range, size, offset, anchor, gravity);
return {std::max(unresized.start, 0), std::min(unresized.end, work_size)};
}
if (adjustments.slide) {
// Either the slide unconstrains the window, or the window is constrained
// in the positive direction
Range1D unslid =
Calculate({/*flip=*/false, /*slide=*/false, /*resize=*/false},
work_size, anchor_range, size, offset, anchor, gravity);
if (unslid.end > work_size)
unslid = unslid.GetTranspose(work_size - unslid.end);
if (unslid.start < 0)
return unslid.GetTranspose(-unslid.start);
return unslid;
}
int32_t start = offset;
switch (anchor) {
case WaylandPositioner::Direction::kNegative:
start += anchor_range.start;
break;
case WaylandPositioner::Direction::kNeutral:
start += anchor_range.center();
break;
case WaylandPositioner::Direction::kPositive:
start += anchor_range.end;
break;
}
switch (gravity) {
case WaylandPositioner::Direction::kNegative:
start -= size;
break;
case WaylandPositioner::Direction::kNeutral:
start -= size / 2;
break;
case WaylandPositioner::Direction::kPositive:
break;
}
return {start, static_cast<int32_t>(start + size)};
}
// The intermediate adjustment results when computing the best positioning for
// the popup.
struct IntermediateAdjustmentResult {
// Result statistics for comparing two different placements.
struct Stats {
// If this is set to false, this result will be chosen iff it is the only
// non-constrained option.
bool preferred;
bool constrained;
int32_t visibility;
} stats;
Range1D position;
ConstraintAdjustment adjustment;
};
// Determines which adjustments (subject to them being a subset of the allowed
// adjustments) result in the best range position.
//
// Note: this is a 1-dimensional projection of the window-positioning problem.
std::pair<Range1D, ConstraintAdjustment> DetermineBestConstraintAdjustment(
const Range1D& work_area,
const Range1D& anchor_range,
uint32_t size,
int32_t offset,
WaylandPositioner::Direction anchor,
WaylandPositioner::Direction gravity,
const ConstraintAdjustment& valid_adjustments,
bool avoid_occlusion) {
if (work_area.start != 0) {
int32_t shift = -work_area.start;
std::pair<Range1D, ConstraintAdjustment> shifted_result =
DetermineBestConstraintAdjustment(
work_area.GetTranspose(shift), anchor_range.GetTranspose(shift),
size, offset, anchor, gravity, valid_adjustments, avoid_occlusion);
return {shifted_result.first.GetTranspose(-shift), shifted_result.second};
}
// To determine the position, cycle through the available combinations of
// adjustments and choose the first one that maximizes the amount of the
// window that is visible on screen. Preferences are given in accordance to
// order when all the stats are equivalent. Therefore, the preference for
// adjustment will be flip > slide > resize.
IntermediateAdjustmentResult best{{/*preferred=*/false,
/*constrained=*/true,
/*visibility=*/0},
/*position=*/{0, 0},
/*adjustment=*/ConstraintAdjustment{}};
bool found_solution = false;
for (uint32_t adjustment_bit_field = 0; adjustment_bit_field < 8;
++adjustment_bit_field) {
// When several options tie for visibility, we preference based on the
// ordering flip > slide > resize, which is defined in the positioner
// specification.
ConstraintAdjustment adjustment =
MaskToConstraintAdjustment(adjustment_bit_field, /*flip_mask=*/1,
/*slide_mask=*/2, /*resize_mask=*/4);
if ((adjustment.flip && !valid_adjustments.flip) ||
(adjustment.slide && !valid_adjustments.slide) ||
(adjustment.resize && !valid_adjustments.resize))
continue;
// When sliding, it can be possible to occlude the parent menu. Therefore,
// this option should not be used if there are better options which have
// acceptable placement.
bool possible_occlusion = false;
if (avoid_occlusion && adjustment.slide)
possible_occlusion = true;
Range1D position = Calculate(adjustment, work_area.end, anchor_range, size,
offset, anchor, gravity);
bool constrained = position.start < 0 || position.end > work_area.end;
int32_t visibility = std::abs(std::min(position.end, work_area.end) -
std::max(position.start, 0));
bool preferred = !possible_occlusion && !constrained;
bool is_better = false;
if (preferred) {
// Always choose a preferred adjustment if the best we have is not
// preferred.
if (!best.stats.preferred || visibility > best.stats.visibility)
is_better = true;
} else {
if (!constrained && best.stats.constrained)
is_better = true;
}
if (is_better) {
found_solution = true;
best = IntermediateAdjustmentResult{
{preferred, constrained, visibility}, position, adjustment};
}
}
// If no solution can be found, allow all transformations. Unfortunately the
// default setting is not valid, because it has a 0x0 dimension.
if (!found_solution && !valid_adjustments.allows_all()) {
ConstraintAdjustment allow_all = {
.flip = true,
.slide = true,
.resize = true,
};
return DetermineBestConstraintAdjustment(work_area, anchor_range, size,
offset, anchor, gravity, allow_all,
avoid_occlusion);
}
DCHECK(found_solution)
<< "Computation is returning without a valid solution. This will result "
"in undefined placement.";
return {best.position, best.adjustment};
}
} // namespace
void WaylandPositioner::SetAnchor(uint32_t anchor) {
std::pair<WaylandPositioner::Direction, WaylandPositioner::Direction>
decompose;
decompose = DecomposeAnchor(anchor);
anchor_x_ = decompose.first;
anchor_y_ = decompose.second;
}
void WaylandPositioner::SetGravity(uint32_t gravity) {
std::pair<WaylandPositioner::Direction, WaylandPositioner::Direction>
decompose;
decompose = DecomposeGravity(gravity);
gravity_x_ = decompose.first;
gravity_y_ = decompose.second;
}
WaylandPositioner::Result WaylandPositioner::CalculateBounds(
const gfx::Rect& work_area) const {
auto anchor_x = anchor_x_;
auto anchor_y = anchor_y_;
auto gravity_x = gravity_x_;
auto gravity_y = gravity_y_;
ConstraintAdjustment adjustments_x = MaskToConstraintAdjustment(
adjustment_, XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X,
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X,
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X);
ConstraintAdjustment adjustments_y = MaskToConstraintAdjustment(
adjustment_, XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y,
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y,
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y);
int32_t offset_x = offset_.x();
int32_t offset_y = offset_.y();
// Exo may prefer some adjustments over others in cases when the orthogonal
// anchor+gravity would mean the slide can occlude |anchor_rect_|, unless it
// already is occluded.
//
// We are doing this in order to stop a common case of clients allowing
// dropdown menus to occlude the menu header. Whilst this may cause some
// popups to avoid sliding where they could, for UX reasons we'd rather that
// than allowing menus to be occluded.
//
// This is best effort. If it is not possible to position the popup within the
// work area, exo might choose to occlude the parent.
bool x_occluded = !(anchor_x == gravity_x && anchor_x != kNeutral);
bool y_occluded = !(anchor_y == gravity_y && anchor_y != kNeutral);
bool avoid_y_occlusion = x_occluded && !y_occluded;
bool avoid_x_occlusion = y_occluded && !x_occluded;
std::pair<Range1D, ConstraintAdjustment> x =
DetermineBestConstraintAdjustment(
{work_area.x(), work_area.right()},
{anchor_rect_.x(), anchor_rect_.right()}, size_.width(), offset_x,
anchor_x, gravity_x, adjustments_x, avoid_x_occlusion);
std::pair<Range1D, ConstraintAdjustment> y =
DetermineBestConstraintAdjustment(
{work_area.y(), work_area.bottom()},
{anchor_rect_.y(), anchor_rect_.bottom()}, size_.height(), offset_y,
anchor_y, gravity_y, adjustments_y, avoid_y_occlusion);
gfx::Point origin(x.first.start, y.first.start);
gfx::Size size(std::max(1, x.first.end - x.first.start),
std::max(1, y.first.end - y.first.start));
return {origin, size};
}
} // namespace exo::wayland
|