.. _layout-creation:

======================
How to create a layout
======================

The aim of this page is to explain the main components of qtile layouts, how
they work, and how you can use them to create your own layouts or hack existing
layouts to make them work the way you want them.

.. note::
    It is highly recommended that users wishing to create their own layout refer
    to the source documentation of existing layouts to familiarise themselves with
    the code.

What is a layout?
=================

In Qtile, a layout is essentially a set of rules that determine how windows should be
displayed on the screen. The layout is responsible for positioning all windows other than
floating windows, "static" windows, internal windows (e.g. the bar) and windows that have
requested not to be managed by the window manager.

Base classes
============

To simplify the creation of layouts, a couple of base classes are available to users.

The ``Layout`` class
~~~~~~~~~~~~~~~~~~~~

As a bare minimum, all layouts should inherit the base ``Layout`` class object as this
class defines a number of methods required for basic usage and will also raise errors if the
required methods are not implemented. Further information on these required methods is set out
below.

The ``_SimpleLayoutBase`` class
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This class implements everything needed for a basic layout with the exception of the
``configure`` method. Therefore, unless your layout requires special logic for updating
and navigating the list of clients, it is strongly recommended that your layout inherits
this base class 

The ``_ClientList`` class
~~~~~~~~~~~~~~~~~~~~~~~~~

This class defines a list of clients and the current client.

The collection is meant as a base or utility class for special layouts,
which need to maintain one or several collections of windows, for example
Columns or Stack, which use this class as base for their internal helper.

The property ``current_index`` get and set the index to the current client,
whereas ``current_client`` property can be used with clients directly.

Required methods
================

To create a minimal, functioning layout your layout must include the methods listed below:

* ``__init__``
* ``configure``
* ``add_client``
* ``remove``
* ``focus_first``
* ``focus_last``
* ``focus_next``
* ``focus_previous``
* ``next``
* ``previous``

As noted above, if you create a layout based on the ``_SimpleLayoutBase`` class, you will only
need to define ``configure`` (and ``_init__``, if you have custom parameters). However, for the
purposes of this document, we will show examples of all required methods.

``__init__``
~~~~~~~~~~~~

Initialise your layout's variables here. The main use of this method will be to load
any default parameters defined by layout. These are defined in a class attribute called
``defaults``. The format of this attribute is a list of tuples.

.. code:: python

    from libqtile.layout import base


    class TwoByTwo(base.Layout):
        """
        A simple layout with a fixed two by two grid.

        By default, unfocused windows are smaller than the focused window.
        """
        defaults = [
            ("border_width", 5, "Window border width"),
            ("border_colour", "00ff00", "Window border colour"),
            ("margin_focused", 5, "Margin width for focused windows"),
            ("margin_unfocused", 50, "Margin width for unfocused windows")
        ]

        def __init__(self, **config):
            base.Layout.__init__(self, **config)
            self.add_defaults(TwoByTwo.defaults)
            self.clients = []
            self.current_client = None

Once the layouts is initialised, these parameters are available at
``self.border_width`` etc.

``configure``
~~~~~~~~~~~~~

This is where the magic happens! This method is responsible for determining how to position
a window on the screen.

This method should therefore configure the dimensions and borders of a window using the
window's ```.place()``` method. The layout can also call either ``hide()`` or ``.unhide()``
on the window.

.. code:: python

    def configure(self, client: Window, screen_rect: ScreenRect) -> None:
        """Simple example breaking screen into four quarters."""
        try:
            index = self.clients.index(client)
        except ValueError:
            # Layout not expecting this window so ignore it
            return

        # We're only showing first 4 windows
        if index > 3:
            client.hide()
            return

        # Unhide the window in case it was hiddent before
        client.unhide()

        # List to help us calculate x and y values of 
        quarters = [
            (0, 0),
            (0.5, 0),
            (0, 0.5),
            (0.5, 0.5)
        ]

        # Calculate size and position for each window
        xpos, ypos = quarters[index]

        x = int(screen_rect.width * xpos) + screen_rect.x
        y = int(screen_rect.height * ypos) + screen_rect.y
        w = screen_rect.width // 2
        h = screen_rect.height // 2

        if client is self.current_client:
            margin = self.margin_focused
        else:
            margin = self.margin_unfocused

        client.place(
            x,
            y,
            w - self.border_width * 2,
            h - self.border_width * 2,
            self.border_width,
            self.border_colour,
            margin=[margin] * 4,
        )

``add_client``
~~~~~~~~~~~~~~

This method is called whenever a window is added to the group, regardless of
whether the layout is current or not. The layout should just add the window to
its internal datastructures, without mapping or configuring/displaying.

.. code:: python

    def add_client(self, client: Window) -> None:
        # Assumes self.clients is simple list
        self.clients.insert(0, client)
        self.current_client = client

``remove``
~~~~~~~~~~

This method is called whenever a window is removed from the group, regardless 
of whether the layout is current or not. The layout should just
de-register the window from its data structures, without unmapping the
window.

The method must also return the "next" window that should gain focus or ``None``
if there are no other windows.

.. code:: python

    def remove(self, client: Window) -> Window | None:
        # Assumes self.clients is a simple list
        # Client already removed so ignore this
        if client not in self.clients:
            return None
        # Client is only window in the list
        elif len(self.clients) == 1:
            self.clients.remove(client)
            self.current_client = None
            # There are no other windows so return None
            return None
        else:
            # Find position of client in our list
            index = self.clients.index(client)
            # Remove client
            self.clients.remove(client)
            # Ensure the index value is not greater than list size
            # i.e. if we closed the last window in the list, we need to return
            # the first one (index 0).
            index %= len(self.clients)
            next_client = self.clients[index]
            self.current_client = next_client
            return next_client

``focus_first``
~~~~~~~~~~~~~~~

This method is called when the first client in the layout should be focused.

This method should just return the first client in the layout, if any. NB the method
should not focus the client itself, this is done by caller.

.. code:: python

    def focus_first(self) -> Window | None:
        if not self.clients:
            return None

        return self.client[0]

``focus_last``
~~~~~~~~~~~~~~

This method is called when the last client in the layout should be focused.

This method should just return the last client in the layout, if any. NB the method
should not focus the client itself, this is done by caller.

.. code:: python

    def focus_last(self) -> Window | None:
        if not self.clients:
            return None

        return self.client[-1]

``focus_next``
~~~~~~~~~~~~~~

This method is called the next client in the layout should be focused.

This method should return the next client in the layout, if any. NB the layout
should not cycle clients when reaching the end of the list as there are other method in
the group for cycling windows which focus floating windows once the the end of the tiled
client list is reached.

In addition, the method should not focus the client.

.. code:: python

    def focus_next(self, win: Window) -> Window | None:
        try:
            return self.clients[self.clients.index(win) + 1]
        except IndexError:
            return None

``focus_previous``
~~~~~~~~~~~~~~~~~~

This method is called the previous client in the layout should be focused.

This method should return the previous client in the layout, if any. NB the layout
should not cycle clients when reaching the end of the list as there are other method in
the group for cycling windows which focus floating windows once the the end of the tiled
client list is reached.

In addition, the method should not focus the client.

.. code:: python

    def focus_previous(self, win: Window) -> Window | None:
        if not self.clients or self.clients.index(win) == 0
            return None

        try:
            return self.clients[self.clients.index(win) - 1]
        except IndexError:
            return None

``next``
~~~~~~~~

This method focuses the next tiled window and can cycle back to the beginning of the list.

.. code:: python

    def next(self) -> None:
        if self.current_client is None:
            return
        # Get the next client or, if at the end of the list, get the first
        client = self.focus_next(self.current_client) or self.focus_first()
        self.group.focus(client, True)

``previous``
~~~~~~~~~~~~

This method focuses the previous tiled window and can cycle back to the end of the list.

.. code:: python

    def previous(self) -> None:
        if self.current_client is None:
            return
        # Get the previous client or, if at the end of the list, get the last
        client = self.focus_previous(self.current_client) or self.focus_last()
        self.group.focus(client, True)

Additional methods
==================

While not essential to implement, the following methods can also be defined:

* ``clone``
* ``show``
* ``hide``
* ``swap``
* ``focus``
* ``blur``

``clone``
~~~~~~~~~

Each group gets a copy of the layout. The ``clone`` method is used to create this copy. The default
implementation in ``Layout`` is as follows:

.. code:: python

    def clone(self, group: _Group) -> Self:
        c = copy.copy(self)
        c._group = group
        return c

``show``
~~~~~~~~

This method can be used to run code when the layout is being displayed. The method receives one argument,
the ``ScreenRect`` for the screen showing the layout.

The default implementation is a no-op:

.. code:: python

    def show(self, screen_rect: ScreenRect) -> None:
        pass

``hide``
~~~~~~~~

This method can be used to run code when the layout is being hidden.

The default implementation is a no-op:

.. code:: python

    def hide(self) -> None:
        pass

``swap``
~~~~~~~~

This method is used to change the position of two windows in the layout.

.. code:: python

    def swap(self, c1: Window, c2: Window) -> None:
        if c1 not in self.clients and c2 not in self.clients:
            return

        index1 = self.clients.index(c1)
        index2 = self.clients.index(c2)

        self.clients[index1], self.clients[index2] = self.clients[index2], self.clients[index1]

``focus``
~~~~~~~~~

This method is called when a given window is being focused.

.. code:: python

    def focus(self, client: Window) -> None:
        if client not in self.clients:
            self.current_client = None
            return

        index = self.clients.index(client)

        # Check if window is not visible
        if index > 3:
            c = self.clients.pop(index)
            self.clients.insert(0, c)

        self.current_client = client

``blur``
~~~~~~~~

This method is called when the layout loses focus.

.. code:: python

    def blur(self) -> None:
        self.current_client = None

Adding commands
===============

Adding commands allows users to modify the behaviour of the layout. To make commands
available via the command interface (e.g. via ``lazy.layout`` calls), the layout must
include the following import:

.. code:: python

    from libqtile.command.base import expose_command

Commands are then decorated with ``@expose_command``. For example:

.. code:: python

    @expose_command
    def rotate(self, clockwise: bool = True) -> None:
        if not self.clients:
            return

        if clockwise:
            client = self.clients.pop(-1)
            self.clients.insert(0, client)
        else:
            client = self.clients.pop(0)
            self.clients.append(client)

        # Check if current client has been rotated off the screen
        if self.current_client and self.clients.index(self.current_client) > 3:
            if clockwise:
                self.current_client = self.clients[3]
            else:
                self.current_client = self.clients[0]

        # Redraw the layout
        self.group.layout_all()

The ``info`` command
~~~~~~~~~~~~~~~~~~~~

Layouts should also implement an ``info`` method to provide information about the layout.

As a minimum, the test suite (see below) will expect a layout to return the following information:

* Its name
* Its group
* The clients managed by the layout

NB the last item is not included in ``Layout``'s implementation of the method so it should be added
when defining a class that inherits that base.

.. code:: python

    @expose_command
    def info(self) -> dict[str, Any]:
        inf = base.Layout.info(self)
        inf["clients"] = self.clients
        return inf

Adding layout to main repo
==========================

If you think your layout is amazing and you want to share with other users by including it in the
main repo then there are a couple of extra steps that you need to take.

Add to list of layouts
~~~~~~~~~~~~~~~~~~~~~~

You must save the layout in ``libqtile/layout`` and then add a line importing the layout definition
to ``libqtile/layout/__init__.py`` e.g.

.. code:: python

    from libqtile.layout.twobytwo import TwoByTwo

Add tests
~~~~~~~~~

Basic functionality for all layouts is handled automatically by the core test suite. However, you
should create tests for any custom functionality of your layout (e.g. testing the ``rotate`` command
defined above).

Full example
============

The full code for the example layout is as follows:

.. code:: python

    from __future__ import annotations

    from typing import TYPE_CHECKING

    from libqtile.command.base import expose_command
    from libqtile.layout import base

    if TYPE_CHECKING:
        from libqtile.backend.base import Window
        from libqtile.config import ScreenRect
        from libqtile.group import _Group


    class TwoByTwo(base.Layout):
        """
        A simple layout with a fixed two by two grid.

        By default, unfocused windows are smaller than the focused window.
        """
        defaults = [
            ("border_width", 5, "Window border width"),
            ("border_colour", "00ff00", "Window border colour"),
            ("margin_focused", 5, "Margin width for focused windows"),
            ("margin_unfocused", 50, "Margin width for unfocused windows")
        ]

        def __init__(self, **config):
            base.Layout.__init__(self, **config)
            self.add_defaults(TwoByTwo.defaults)
            self.clients = []
            self.current_client = None

        def configure(self, client: Window, screen_rect: ScreenRect) -> None:
            """Simple example breaking screen into four quarters."""
            try:
                index = self.clients.index(client)
            except ValueError:
                # Layout not expecting this window so ignore it
                return

            # We're only showing first 4 windows
            if index > 3:
                client.hide()
                return

            # Unhide the window in case it was hiddent before
            client.unhide()

            # List to help us calculate x and y values of 
            quarters = [
                (0, 0),
                (0.5, 0),
                (0, 0.5),
                (0.5, 0.5)
            ]

            # Calculate size and position for each window
            xpos, ypos = quarters[index]

            x = int(screen_rect.width * xpos) + screen_rect.x
            y = int(screen_rect.height * ypos) + screen_rect.y
            w = screen_rect.width // 2
            h = screen_rect.height // 2

            if client is self.current_client:
                margin = self.margin_focused
            else:
                margin = self.margin_unfocused

            client.place(
                x,
                y,
                w - self.border_width * 2,
                h - self.border_width * 2,
                self.border_width,
                self.border_colour,
                margin=[margin] * 4,
            )

        def add_client(self, client: Window) -> None:
            # Assumes self.clients is simple list
            self.clients.insert(0, client)
            self.current_client = client

        def remove(self, client: Window) -> Window | None:
            # Assumes self.clients is a simple list
            # Client already removed so ignore this
            if client not in self.clients:
                return None
            # Client is only window in the list
            elif len(self.clients) == 1:
                self.clients.remove(client)
                self.current_client = None
                # There are no other windows so return None
                return None
            else:
                # Find position of client in our list
                index = self.clients.index(client)
                # Remove client
                self.clients.remove(client)
                # Ensure the index value is not greater than list size
                # i.e. if we closed the last window in the list, we need to return
                # the first one (index 0).
                index %= len(self.clients)
                next_client = self.clients[index]
                self.current_client = next_client
                return next_client

        def focus_first(self) -> Window | None:
            if not self.clients:
                return None

            return self.client[0]

        def focus_last(self) -> Window | None:
            if not self.clients:
                return None

            return self.client[-1]

        def focus_next(self, win: Window) -> Window | None:
            try:
                return self.clients[self.clients.index(win) + 1]
            except IndexError:
                return None

        def focus_previous(self, win: Window) -> Window | None:
            if not self.clients or self.clients.index(win) == 0:
                return None

            try:
                return self.clients[self.clients.index(win) - 1]
            except IndexError:
                return None

        def next(self) -> None:
            if self.current_client is None:
                return
            # Get the next client or, if at the end of the list, get the first
            client = self.focus_next(self.current_client) or self.focus_first()
            self.group.focus(client, True)

        def previous(self) -> None:
            if self.current_client is None:
                return
            # Get the previous client or, if at the end of the list, get the last
            client = self.focus_previous(self.current_client) or self.focus_last()
            self.group.focus(client, True)

        def swap(self, c1: Window, c2: Window) -> None:
            if c1 not in self.clients and c2 not in self.clients:
                return

            index1 = self.clients.index(c1)
            index2 = self.clients.index(c2)

            self.clients[index1], self.clients[index2] = self.clients[index2], self.clients[index1]

        def focus(self, client: Window) -> None:
            if client not in self.clients:
                self.current_client = None
                return

            index = self.clients.index(client)

            # Check if window is not visible
            if index > 3:
                c = self.clients.pop(index)
                self.clients.insert(0, c)

            self.current_client = client

        def blur(self) -> None:
            self.current_client = None

        @expose_command
        def rotate(self, clockwise: bool = True) -> None:
            if not self.clients:
                return

            if clockwise:
                client = self.clients.pop(-1)
                self.clients.insert(0, client)
            else:
                client = self.clients.pop(0)
                self.clients.append(client)

            # Check if current client has been rotated off the screen
            if self.current_client and self.clients.index(self.current_client) > 3:
                if clockwise:
                    self.current_client = self.clients[3]
                else:
                    self.current_client = self.clients[0]

            # Redraw the layout
            self.group.layout_all()

        @expose_command
        def info(self) -> dict[str, Any]:
            inf = base.Layout.info(self)
            inf["clients"] = self.clients
            return inf


This should result in a layout looking like this: |layout_image|.

.. |layout_image| image:: ../../_static/layouts/twobytwo.png

Getting help
============

If you still need help with developing your widget then please submit a question in the
`qtile-dev group <https://groups.google.com/forum/#!forum/qtile-dev>`_ or submit an issue
on the github page if you believe there's an error in the codebase.


