This commit is contained in:
Will McGugan 2021-02-15 12:04:26 +00:00
parent 0a5315a211
commit 9a29b36514
5 changed files with 173 additions and 66 deletions

View file

@ -1,7 +1,7 @@
Layout
======
Rich offers a :class:`~rich.layout.Layout` class which can be used to divide the screen area in to parts, where each part can contain its own content. It is typically used with :ref:`Live` to create full-screen "applications", but may be used standalone.
Rich offers a :class:`~rich.layout.Layout` class which can be used to divide the screen area in to parts, where each part may contain independent content. It is often used with :ref:`Live` to create full-screen "applications" but may be used standalone.
To see an example of a Layout, run the following from the command line::
@ -18,7 +18,7 @@ To define a layout, construct a Layout object and print it::
layout = Layout()
print(layout)
This will draw a box the size of the terminal with some information regarding the layout. The box is a "placeholder" because we have yet to add any content to it. Before we do that, we can call the :meth:`~rich.layout.Layout.split` method to divide the layout in to two sub-layouts::
This will draw a box the size of the terminal with some information regarding the layout. The box is a "placeholder" because we have yet to add any content to it. Before we do that, let's create a more interesting layout by calling the :meth:`~rich.layout.Layout.split` method to divide the layout in to two sub-layouts::
layout.split(
Layout(name="upper"),
@ -37,12 +37,52 @@ This will divide the terminal screen in to two equal sized portions, one on top
The addition of the ``direction="horizontal"`` tells the Layout class to split left-to-right, rather than the default of top-to-bottom.
You should now see the screen area divided in to 3 portions; an upper half and a lower half that is split in to two quarters. You can continue to call split() in this way to create as many parts to the screen as you wish.
You should now see the screen area divided in to 3 portions; an upper half and a lower half that is split in to two quarters.
Updating renderables
--------------------
.. raw:: html
The Layout class would not be that useful if it only displayed placeholders. Fortunately we can tell the layout (or sub-layout) to display text or any other renderable in it's area of the screen by calling :meth:`~rich.layout.Layout.update`. Here is an example::
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #000080">╭─────────────────────────────── </span><span style="color: #008000">'upper'</span><span style="color: #000080"> </span><span style="color: #000080; font-weight: bold">(</span><span style="color: #000080; font-weight: bold">84</span><span style="color: #000080"> x </span><span style="color: #000080; font-weight: bold">13</span><span style="color: #000080; font-weight: bold">)</span><span style="color: #000080"> ────────────────────────────────╮</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="font-weight: bold">{</span><span style="color: #008000">'size'</span>: <span style="color: #800080; font-style: italic">None</span>, <span style="color: #008000">'minimum_size'</span>: <span style="color: #000080; font-weight: bold">1</span>, <span style="color: #008000">'ratio'</span>: <span style="color: #000080; font-weight: bold">1</span>, <span style="color: #008000">'name'</span>: <span style="color: #008000">'upper'</span><span style="font-weight: bold">}</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">│</span>
<span style="color: #000080">╰──────────────────────────────────────────────────────────────────────────────────╯</span>
<span style="color: #000080">╭─────────── </span><span style="color: #008000">'left'</span><span style="color: #000080"> </span><span style="color: #000080; font-weight: bold">(</span><span style="color: #000080; font-weight: bold">42</span><span style="color: #000080"> x </span><span style="color: #000080; font-weight: bold">14</span><span style="color: #000080; font-weight: bold">)</span><span style="color: #000080"> ───────────╮╭────────── </span><span style="color: #008000">'right'</span><span style="color: #000080"> </span><span style="color: #000080; font-weight: bold">(</span><span style="color: #000080; font-weight: bold">42</span><span style="color: #000080"> x </span><span style="color: #000080; font-weight: bold">14</span><span style="color: #000080; font-weight: bold">)</span><span style="color: #000080"> ───────────╮</span>
<span style="color: #000080">│</span> <span style="color: #000080">││</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">││</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">││</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="font-weight: bold">{</span> <span style="color: #000080">││</span> <span style="font-weight: bold">{</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #008000">'size'</span>: <span style="color: #800080; font-style: italic">None</span>, <span style="color: #000080">││</span> <span style="color: #008000">'size'</span>: <span style="color: #800080; font-style: italic">None</span>, <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #008000">'minimum_size'</span>: <span style="color: #000080; font-weight: bold">1</span>, <span style="color: #000080">││</span> <span style="color: #008000">'minimum_size'</span>: <span style="color: #000080; font-weight: bold">1</span>, <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #008000">'ratio'</span>: <span style="color: #000080; font-weight: bold">1</span>, <span style="color: #000080">││</span> <span style="color: #008000">'ratio'</span>: <span style="color: #000080; font-weight: bold">1</span>, <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #008000">'name'</span>: <span style="color: #008000">'left'</span> <span style="color: #000080">││</span> <span style="color: #008000">'name'</span>: <span style="color: #008000">'right'</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="font-weight: bold">}</span> <span style="color: #000080">││</span> <span style="font-weight: bold">}</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">││</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">││</span> <span style="color: #000080">│</span>
<span style="color: #000080">│</span> <span style="color: #000080">││</span> <span style="color: #000080">│</span>
<span style="color: #000080">╰────────────────────────────────────────╯╰────────────────────────────────────────╯</span>
</pre>
You can continue to call split() in this way to create as many parts to the screen as you wish.
Setting renderables
-------------------
The first position argument to ``Layout`` can be any Rich renderable, which will be sized to fit within the layout's area. Here's how we might divide the "right" layout in to two panels::
layout["right"].split(
Layout(Panel("Hello")),
Layout(Panel("World!))
)
You can also call :meth:`~rich.layout.Layout.update` to set or replace the current renderable::
layout["left"].update("The mystery of life isn't a problem to solve, but a reality to experience.")
print(layout)
@ -50,7 +90,7 @@ The Layout class would not be that useful if it only displayed placeholders. For
Fixed size
----------
You can set a sub-layout to use a fixed size by setting the ``size`` argument on the Layout constructor or by setting the attribute. Here's an example::
You can set a layout to use a fixed size by setting the ``size`` argument on the Layout constructor or by setting the attribute. Here's an example::
layout["upper"].size = 10
print(layout)
@ -60,7 +100,7 @@ This will set the upper portion to be exactly 10 rows, no matter the size of the
Ratio
-----
In addition to a fixed size, you can also assign a *ratio* to a Layout in the constructor or by assigning to the attribute. The ratio defines how much of the screen the layout should occupy in relation to other layouts. For example, lets reset the size and set the ratio of the upper layout to 2::
In addition to a fixed size, you can also make a flexible layout setting the ``ratio`` argument on the constructor or by assigning to the attribute. The ratio defines how much of the screen the layout should occupy in relation to other layouts. For example, lets reset the size and set the ratio of the upper layout to 2::
layout["upper"].size = None
layout["upper"].ratio = 2
@ -75,7 +115,7 @@ A layout with a ratio set may also have a minimum size to prevent it from gettin
Visibility
----------
Sub-layouts are visible by default, but you can make a layout invisible by setting the ``visible`` attribute to False. Here's an example::
You can make a layout invisible by setting the ``visible`` attribute to False. Here's an example::
layout["upper"].visible = False
print(layout)
@ -85,10 +125,17 @@ The top layout is now invisible, and the "lower" layout will expand to fill the
layout["upper"].visible = True
print(layout)
You could use this to toggle parts of your interface based on your applications configuration.
Tree
----
To help visualize complex layouts you can print the ``tree`` attribute which will display a summary of the layout with a tree::
To help visualize complex layouts you can print the ``tree`` attribute which will display a summary of the layout as a tree::
print(layout.tree)
Example
-------
See `fullscreen.py <https://github.com/willmcgugan/rich/blob/master/examples/fullscreen.py>`_ for an example that combines :class:`~rich.layout.Layout` and :class:`rich.live.Live` to create a fullscreen "application".

View file

@ -75,7 +75,7 @@ Alternate screen
You can opt to show a Live display in the "alternate screen" by setting ``screen=False`` on the constructor. This will allow your live display to go full screen and restore the command prompt on exit.
You can use this feature in combination with :ref:`Layoout` to display sophisticated terminal "applications".
You can use this feature in combination with :ref:`Layout` to display sophisticated terminal "applications".
Transient display
~~~~~~~~~~~~~~~~~

View file

@ -1,17 +1,24 @@
"""
Demonstrates a Rich "application" using the Layout and Live classes.
"""
from datetime import datetime
from rich import box
from rich.console import Console
from rich.layout import Layout
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
from rich.syntax import Syntax
from rich.table import Table
from rich.__main__ import make_test_card
from rich.text import Text
console = Console()
def make_layout() -> Layout:
"""Define the layout."""
layout = Layout(name="root")
layout.split(
@ -28,7 +35,54 @@ def make_layout() -> Layout:
return layout
def make_sponsor_message() -> Panel:
"""Some example content."""
sponsor_message = Table.grid(padding=1)
sponsor_message.add_column(style="green", justify="right")
sponsor_message.add_column(no_wrap=True)
sponsor_message.add_row(
"Sponsor me",
"[u blue link=https://github.com/sponsors/willmcgugan]https://github.com/sponsors/willmcgugan",
)
sponsor_message.add_row(
"Buy me a :coffee:",
"[u blue link=https://ko-fi.com/willmcgugan]https://ko-fi.com/willmcgugan",
)
sponsor_message.add_row(
"Twitter",
"[u blue link=https://twitter.com/willmcgugan]https://twitter.com/willmcgugan",
)
sponsor_message.add_row(
"Blog", "[u blue link=https://www.willmcgugan.com]https://www.willmcgugan.com"
)
intro_message = Text.from_markup(
"""\
It takes a lot of time to develop Rich and to provide support.
Consider supporting my work via Github Sponsors (ask your company / organization), or buy me a coffee to say thanks.
- Will McGugan"""
)
message = Table.grid(padding=2)
message.add_column()
message.add_column(no_wrap=True)
message.add_row(intro_message, sponsor_message)
message_panel = Panel.fit(
message,
box=box.ROUNDED,
padding=(1, 2),
title="[b red]Thanks for trying out Rich!",
border_style="bright_blue",
)
return message_panel
class Header:
"""Display header with clock."""
def __rich__(self) -> Panel:
grid = Table.grid(expand=True)
grid.add_column(justify="center", ratio=1)
@ -40,56 +94,48 @@ class Header:
return Panel(grid, style="white on blue")
code = """\
def ratio_resolve(total: int, edges: List[Edge]) -> List[int]:
sizes = [(edge.size or None) for edge in edges]
def make_syntax() -> Syntax:
code = """\
def ratio_resolve(total: int, edges: List[Edge]) -> List[int]:
sizes = [(edge.size or None) for edge in edges]
# While any edges haven't been calculated
while any(size is None for size in sizes):
# Get flexible edges and index to map these back on to sizes list
flexible_edges = [
(index, edge)
for index, (size, edge) in enumerate(zip(sizes, edges))
if size is None
]
# Remaining space in total
remaining = total - sum(size or 0 for size in sizes)
if remaining <= 0:
# No room for flexible edges
sizes[:] = [(size or 0) for size in sizes]
break
# Calculate number of characters in a ratio portion
portion = remaining / sum((edge.ratio or 1) for _, edge in flexible_edges)
# If any edges will be less than their minimum, replace size with the minimum
for index, edge in flexible_edges:
if portion * edge.ratio <= edge.minimum_size:
sizes[index] = edge.minimum_size
# While any edges haven't been calculated
while any(size is None for size in sizes):
# Get flexible edges and index to map these back on to sizes list
flexible_edges = [
(index, edge)
for index, (size, edge) in enumerate(zip(sizes, edges))
if size is None
]
# Remaining space in total
remaining = total - sum(size or 0 for size in sizes)
if remaining <= 0:
# No room for flexible edges
sizes[:] = [(size or 0) for size in sizes]
break
else:
# Distribute flexible space and compensate for rounding error
# Since edge sizes can only be integers we need to add the remainder
# to the following line
_modf = modf
remainder = 0.0
# Calculate number of characters in a ratio portion
portion = remaining / sum((edge.ratio or 1) for _, edge in flexible_edges)
# If any edges will be less than their minimum, replace size with the minimum
for index, edge in flexible_edges:
remainder, size = _modf(portion * edge.ratio + remainder)
sizes[index] = int(size)
break
# Sizes now contains integers only
return cast(List[int], sizes)
"""
syntax = Syntax(code, "python", line_numbers=True)
layout = make_layout()
layout["header"].update(Header())
layout["body"].update(make_test_card())
layout["box2"].update(Panel(syntax, border_style="green"))
layout["box1"].update(Panel(layout.tree, border_style="red"))
if portion * edge.ratio <= edge.minimum_size:
sizes[index] = edge.minimum_size
break
else:
# Distribute flexible space and compensate for rounding error
# Since edge sizes can only be integers we need to add the remainder
# to the following line
_modf = modf
remainder = 0.0
for index, edge in flexible_edges:
remainder, size = _modf(portion * edge.ratio + remainder)
sizes[index] = int(size)
break
# Sizes now contains integers only
return cast(List[int], sizes)
"""
syntax = Syntax(code, "python", line_numbers=True)
return syntax
job_progress = Progress(
@ -98,9 +144,9 @@ job_progress = Progress(
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
)
job1 = job_progress.add_task("[green]Cooking")
job2 = job_progress.add_task("[magenta]Baking", total=200)
job3 = job_progress.add_task("[cyan]Mixing", total=400)
job_progress.add_task("[green]Cooking")
job_progress.add_task("[magenta]Baking", total=200)
job_progress.add_task("[cyan]Mixing", total=400)
total = sum(task.total for task in job_progress.tasks)
overall_progress = Progress()
@ -109,11 +155,20 @@ overall_task = overall_progress.add_task("All Jobs", total=int(total))
progress_table = Table.grid(expand=True)
progress_table.add_row(
Panel(
overall_progress, title="Overall Progress", border_style="green", padding=(2, 2)
overall_progress,
title="Overall Progress",
border_style="green",
padding=(2, 2),
),
Panel(job_progress, title="[b]Jobs", border_style="red", padding=(1, 2)),
)
layout = make_layout()
layout["header"].update(Header())
layout["body"].update(make_sponsor_message())
layout["box2"].update(Panel(make_syntax(), border_style="green"))
layout["box1"].update(Panel(layout.tree, border_style="red"))
layout["footer"].update(progress_table)

View file

@ -135,12 +135,16 @@ class Layout:
def summary(layout) -> "Text":
name = repr(layout.name) + " " if layout.name else ""
direction = "" if layout.direction == "horizontal" else ""
direction = (
("" if layout.direction == "horizontal" else "")
if layout._children
else ""
)
if layout.size:
_summary = highlighter(f"{direction} {name}(size={layout.size})")
else:
_summary = highlighter(f"{direction} {name}(ratio={layout.ratio})")
_summary.stylize("dim" if layout.visible else "")
_summary.stylize("" if layout.visible else "dim")
return _summary
layout = self

View file

@ -892,6 +892,7 @@ class Progress(JupyterMixin):
return table
def __rich__(self) -> RenderableType:
"""Makes the Progress class itself renderable."""
return self.get_renderable()
def add_task(