# Source code for quicktile.layout

```"""Layout calculation code"""

__author__ = "Stephan Sokolow (deitarion/SSokolow)"
__license__ = "GNU GPL 2.0 or later"

# Silence PyLint being flat-out wrong about MyPy type annotations and
# complaining about my grouped imports
# pylint: disable=unsubscriptable-object
# pylint: disable=wrong-import-order

import math

from .util import Gravity, Rectangle

# -- Type-Annotation Imports --
from typing import Dict, List, Union
from .util import GeomTuple, PercentRectTuple

#: MyPy type alias for either `Rectangle` or `GeomTuple`
Geom = Union[Rectangle, GeomTuple]  # pylint: disable=invalid-name
# --

[docs]def check_tolerance(distance: int, monitor_geom: Rectangle,
tolerance: float=0.1) -> float:
"""Check whether a distance is within a tolerance value calculated as a
percentage of a monitor's size.

:param distance: A distance in pixels.
:param monitor_geom: A ``Rectangle`` representing the monitor geometry.
:param tolerance: A value between 0.0 and 1.0, inclusive, which represents
a percentage of the monitor size.

.. note:: This is not currently in use but is retained for when future
plans make it necessary to design reliable "invalidate cached data if
the window was repositioned/resized without QuickTile" code.
"""

# Take the euclidean distance of the monitor's width and height and convert
# `distance` into a percentage of it, then test against `tolerance`.
return (float(distance) /
math.hypot(monitor_geom.width, monitor_geom.height)
) < tolerance

[docs]def resolve_fractional_geom(fract_geom: Union[PercentRectTuple, Rectangle],
monitor_rect: Rectangle) -> Rectangle:
"""Resolve proportional (eg. ``0.5``) and preserved (``None``) coordinates.

:param fract_geom: An ``(x, y, w, h)`` tuple containing monitor-relative
values in the range from 0.0 to 1.0, inclusive, or a
:class:`quicktile.util.Rectangle` which will be passed through without
modification.
:param monitor_rect: A :class:`quicktile.util.Rectangle` defining the
bounding box of the monitor (or other desired region) within the
desktop.
:returns: A rectangle with absolute coordinates derived from
``monitor_rect``.
"""
if isinstance(fract_geom, Rectangle):
return fract_geom
else:
return Rectangle(
x=fract_geom * monitor_rect.width,
y=fract_geom * monitor_rect.height,
width=fract_geom * monitor_rect.width,
height=fract_geom * monitor_rect.height)

[docs]class GravityLayout(object):  # pylint: disable=too-few-public-methods
"""Helper for translating top-left relative dimensions to other corners.

Used to generate :func:`quicktile.commands.cycle_dimensions` presets.

Expects to operate on decimal percentage values. (0 ≤ x ≤ 1)

:param margin_x: Horizontal margin to apply when calculating window
positions, as decimal percentage of screen width.
:param margin_y: Vertical margin to apply when calculating window
positions, as decimal percentage of screen height.

"""
# pylint: disable=no-member
#: A mapping of possible window alignments relative to the monitor/desktop
#: as a mapping from formerly manually specified command names to values
#: the :any:`quicktile.util.Gravity` enum can take on.
#:
#: .. todo:: Look into whether I can factor :any:`GRAVITIES` away entirely.
GRAVITIES = dict((x.lower().replace('_', '-'), getattr(Gravity, x)) for
x in Gravity.__members__)  # type: Dict[str, Gravity]

def __init__(self, margin_x=0, margin_y=0):  # type: (int, int) -> None
self.margin_x = margin_x
self.margin_y = margin_y

# pylint: disable=too-many-arguments
[docs]    def __call__(self,
width: float,
height: float,
gravity: str='top-left',
x: float=None,
y: float=None
) -> PercentRectTuple:
"""Return a relative ``(x, y, w, h)`` tuple relative to ``gravity``.

This function takes and returns percentages, represented as decimals
in the range ``0 ≤ x ≤ 1``, which can be multiplied by width and
height values in actual units to produce actual window geometry.

It can be used in two ways:

1. If called **without** ``x`` and ``y`` values, it will compute a
geometry tuple which will align a window ``w`` wide and ``h`` tall
according to ``geometry``.

2. If called **with** ``x`` and ``y`` values, it will translate a
geometry tuple which is relative to the top-left corner so that it is
instead relative to another corner.

:param width: Desired width as a decimal-form percentage
:param height: Desired height as a decimal-form percentage
:param gravity: Desired window alignment from :any:`GRAVITIES`
:param x: Desired horizontal position if not the same as ``gravity``
:param y: Desired vertical position if not the same as ``gravity``

:returns: ``(x, y, w, h)`` with all values represented as decimal-form
percentages.

.. todo:: Consider writing a percentage-based equivalent to
:class:`quicktile.util.Rectangle`.
"""

x = x or self.GRAVITIES[gravity].value
y = y or self.GRAVITIES[gravity].value
offset_x = width * self.GRAVITIES[gravity].value
offset_y = height * self.GRAVITIES[gravity].value

return (round(x - offset_x + self.margin_x, 3),
round(y - offset_y + self.margin_y, 3),
round(width - (self.margin_x * 2), 3),
round(height - (self.margin_y * 2), 3))

[docs]def make_winsplit_positions(columns: int) -> Dict[str, List[PercentRectTuple]]:
"""Generate the classic WinSplit Revolution tiling presets

:params columns: The number of columns that each tiling preset should be
built around.
:return: A dict of presets ready to feed into

See :ref:`ColumnCount <ColumnCount>` in the configuration section of the
manual for further details.

.. todo:: Plumb :meth:`GravityLayout` arguments into the config
file and figure out how to generalize :func:`make_winsplit_positions`
into user-customizable stuff as much as possible.
"""

gvlay = GravityLayout()
col_width = 1.0 / columns
cycle_steps = tuple(round(col_width * x, 3)
for x in range(1, columns))

center_steps = (1.0,) + cycle_steps
edge_steps = (0.5,) + cycle_steps

positions = {
'center': [gvlay(width, 1, 'center') for width in center_steps],
}

for grav in ('top', 'bottom'):
positions[grav] = [gvlay(width, 0.5, grav) for width in center_steps]
for grav in ('left', 'right'):
positions[grav] = [gvlay(width, 1, grav) for width in edge_steps]
for grav in ('top-left', 'top-right', 'bottom-left', 'bottom-right'):
positions[grav] = [gvlay(width, 0.5, grav) for width in edge_steps]

return positions
```