wayland_positioner.cc 16 KB

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