align.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. from itertools import chain
  2. from typing import TYPE_CHECKING, Iterable, Optional, Literal
  3. from .constrain import Constrain
  4. from .jupyter import JupyterMixin
  5. from .measure import Measurement
  6. from .segment import Segment
  7. from .style import StyleType
  8. if TYPE_CHECKING:
  9. from .console import Console, ConsoleOptions, RenderableType, RenderResult
  10. AlignMethod = Literal["left", "center", "right"]
  11. VerticalAlignMethod = Literal["top", "middle", "bottom"]
  12. class Align(JupyterMixin):
  13. """Align a renderable by adding spaces if necessary.
  14. Args:
  15. renderable (RenderableType): A console renderable.
  16. align (AlignMethod): One of "left", "center", or "right""
  17. style (StyleType, optional): An optional style to apply to the background.
  18. vertical (Optional[VerticalAlignMethod], optional): Optional vertical align, one of "top", "middle", or "bottom". Defaults to None.
  19. pad (bool, optional): Pad the right with spaces. Defaults to True.
  20. width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
  21. height (int, optional): Set height of align renderable, or None to fit to contents. Defaults to None.
  22. Raises:
  23. ValueError: if ``align`` is not one of the expected values.
  24. Example:
  25. .. code-block:: python
  26. from rich.console import Console
  27. from rich.align import Align
  28. from rich.panel import Panel
  29. console = Console()
  30. # Create a panel 20 characters wide
  31. p = Panel("Hello, [b]World[/b]!", style="on green", width=20)
  32. # Renders the panel centered in the terminal
  33. console.print(Align(p, align="center"))
  34. """
  35. def __init__(
  36. self,
  37. renderable: "RenderableType",
  38. align: AlignMethod = "left",
  39. style: Optional[StyleType] = None,
  40. *,
  41. vertical: Optional[VerticalAlignMethod] = None,
  42. pad: bool = True,
  43. width: Optional[int] = None,
  44. height: Optional[int] = None,
  45. ) -> None:
  46. if align not in ("left", "center", "right"):
  47. raise ValueError(
  48. f'invalid value for align, expected "left", "center", or "right" (not {align!r})'
  49. )
  50. if vertical is not None and vertical not in ("top", "middle", "bottom"):
  51. raise ValueError(
  52. f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})'
  53. )
  54. self.renderable = renderable
  55. self.align = align
  56. self.style = style
  57. self.vertical = vertical
  58. self.pad = pad
  59. self.width = width
  60. self.height = height
  61. def __repr__(self) -> str:
  62. return f"Align({self.renderable!r}, {self.align!r})"
  63. @classmethod
  64. def left(
  65. cls,
  66. renderable: "RenderableType",
  67. style: Optional[StyleType] = None,
  68. *,
  69. vertical: Optional[VerticalAlignMethod] = None,
  70. pad: bool = True,
  71. width: Optional[int] = None,
  72. height: Optional[int] = None,
  73. ) -> "Align":
  74. """Align a renderable to the left."""
  75. return cls(
  76. renderable,
  77. "left",
  78. style=style,
  79. vertical=vertical,
  80. pad=pad,
  81. width=width,
  82. height=height,
  83. )
  84. @classmethod
  85. def center(
  86. cls,
  87. renderable: "RenderableType",
  88. style: Optional[StyleType] = None,
  89. *,
  90. vertical: Optional[VerticalAlignMethod] = None,
  91. pad: bool = True,
  92. width: Optional[int] = None,
  93. height: Optional[int] = None,
  94. ) -> "Align":
  95. """Align a renderable to the center."""
  96. return cls(
  97. renderable,
  98. "center",
  99. style=style,
  100. vertical=vertical,
  101. pad=pad,
  102. width=width,
  103. height=height,
  104. )
  105. @classmethod
  106. def right(
  107. cls,
  108. renderable: "RenderableType",
  109. style: Optional[StyleType] = None,
  110. *,
  111. vertical: Optional[VerticalAlignMethod] = None,
  112. pad: bool = True,
  113. width: Optional[int] = None,
  114. height: Optional[int] = None,
  115. ) -> "Align":
  116. """Align a renderable to the right."""
  117. return cls(
  118. renderable,
  119. "right",
  120. style=style,
  121. vertical=vertical,
  122. pad=pad,
  123. width=width,
  124. height=height,
  125. )
  126. def __rich_console__(
  127. self, console: "Console", options: "ConsoleOptions"
  128. ) -> "RenderResult":
  129. align = self.align
  130. width = console.measure(self.renderable, options=options).maximum
  131. rendered = console.render(
  132. Constrain(
  133. self.renderable, width if self.width is None else min(width, self.width)
  134. ),
  135. options.update(height=None),
  136. )
  137. lines = list(Segment.split_lines(rendered))
  138. width, height = Segment.get_shape(lines)
  139. lines = Segment.set_shape(lines, width, height)
  140. new_line = Segment.line()
  141. excess_space = options.max_width - width
  142. style = console.get_style(self.style) if self.style is not None else None
  143. def generate_segments() -> Iterable[Segment]:
  144. if excess_space <= 0:
  145. # Exact fit
  146. for line in lines:
  147. yield from line
  148. yield new_line
  149. elif align == "left":
  150. # Pad on the right
  151. pad = Segment(" " * excess_space, style) if self.pad else None
  152. for line in lines:
  153. yield from line
  154. if pad:
  155. yield pad
  156. yield new_line
  157. elif align == "center":
  158. # Pad left and right
  159. left = excess_space // 2
  160. pad = Segment(" " * left, style)
  161. pad_right = (
  162. Segment(" " * (excess_space - left), style) if self.pad else None
  163. )
  164. for line in lines:
  165. if left:
  166. yield pad
  167. yield from line
  168. if pad_right:
  169. yield pad_right
  170. yield new_line
  171. elif align == "right":
  172. # Padding on left
  173. pad = Segment(" " * excess_space, style)
  174. for line in lines:
  175. yield pad
  176. yield from line
  177. yield new_line
  178. blank_line = (
  179. Segment(f"{' ' * (self.width or options.max_width)}\n", style)
  180. if self.pad
  181. else Segment("\n")
  182. )
  183. def blank_lines(count: int) -> Iterable[Segment]:
  184. if count > 0:
  185. for _ in range(count):
  186. yield blank_line
  187. vertical_height = self.height or options.height
  188. iter_segments: Iterable[Segment]
  189. if self.vertical and vertical_height is not None:
  190. if self.vertical == "top":
  191. bottom_space = vertical_height - height
  192. iter_segments = chain(generate_segments(), blank_lines(bottom_space))
  193. elif self.vertical == "middle":
  194. top_space = (vertical_height - height) // 2
  195. bottom_space = vertical_height - top_space - height
  196. iter_segments = chain(
  197. blank_lines(top_space),
  198. generate_segments(),
  199. blank_lines(bottom_space),
  200. )
  201. else: # self.vertical == "bottom":
  202. top_space = vertical_height - height
  203. iter_segments = chain(blank_lines(top_space), generate_segments())
  204. else:
  205. iter_segments = generate_segments()
  206. if self.style:
  207. style = console.get_style(self.style)
  208. iter_segments = Segment.apply_style(iter_segments, style)
  209. yield from iter_segments
  210. def __rich_measure__(
  211. self, console: "Console", options: "ConsoleOptions"
  212. ) -> Measurement:
  213. measurement = Measurement.get(console, options, self.renderable)
  214. return measurement
  215. class VerticalCenter(JupyterMixin):
  216. """Vertically aligns a renderable.
  217. Warn:
  218. This class is deprecated and may be removed in a future version. Use Align class with
  219. `vertical="middle"`.
  220. Args:
  221. renderable (RenderableType): A renderable object.
  222. style (StyleType, optional): An optional style to apply to the background. Defaults to None.
  223. """
  224. def __init__(
  225. self,
  226. renderable: "RenderableType",
  227. style: Optional[StyleType] = None,
  228. ) -> None:
  229. self.renderable = renderable
  230. self.style = style
  231. def __repr__(self) -> str:
  232. return f"VerticalCenter({self.renderable!r})"
  233. def __rich_console__(
  234. self, console: "Console", options: "ConsoleOptions"
  235. ) -> "RenderResult":
  236. style = console.get_style(self.style) if self.style is not None else None
  237. lines = console.render_lines(
  238. self.renderable, options.update(height=None), pad=False
  239. )
  240. width, _height = Segment.get_shape(lines)
  241. new_line = Segment.line()
  242. height = options.height or options.size.height
  243. top_space = (height - len(lines)) // 2
  244. bottom_space = height - top_space - len(lines)
  245. blank_line = Segment(f"{' ' * width}", style)
  246. def blank_lines(count: int) -> Iterable[Segment]:
  247. for _ in range(count):
  248. yield blank_line
  249. yield new_line
  250. if top_space > 0:
  251. yield from blank_lines(top_space)
  252. for line in lines:
  253. yield from line
  254. yield new_line
  255. if bottom_space > 0:
  256. yield from blank_lines(bottom_space)
  257. def __rich_measure__(
  258. self, console: "Console", options: "ConsoleOptions"
  259. ) -> Measurement:
  260. measurement = Measurement.get(console, options, self.renderable)
  261. return measurement
  262. if __name__ == "__main__": # pragma: no cover
  263. from rich.console import Console, Group
  264. from rich.highlighter import ReprHighlighter
  265. from rich.panel import Panel
  266. highlighter = ReprHighlighter()
  267. console = Console()
  268. panel = Panel(
  269. Group(
  270. Align.left(highlighter("align='left'")),
  271. Align.center(highlighter("align='center'")),
  272. Align.right(highlighter("align='right'")),
  273. ),
  274. width=60,
  275. style="on dark_blue",
  276. title="Align",
  277. )
  278. console.print(
  279. Align.center(panel, vertical="middle", style="on red", height=console.height)
  280. )