Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

307 строки
10 KiB

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