Skip to content

Commit 365abe6

Browse files
authored
Merge pull request #5197 from Textualize/content
New render protocol
2 parents 5de3a80 + d426e69 commit 365abe6

File tree

333 files changed

+21494
-19757
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

333 files changed

+21494
-19757
lines changed

src/textual/_segment_tools.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,6 @@ def blank_lines(count: int) -> list[list[Segment]]:
231231
if top_blank_lines:
232232
yield from blank_lines(top_blank_lines)
233233

234-
horizontal_excess_space = max(0, width - shape_width)
235-
236234
if horizontal == "left":
237235
for cell_length, line in zip(line_lengths, lines):
238236
if cell_length == width:
@@ -241,7 +239,7 @@ def blank_lines(count: int) -> list[list[Segment]]:
241239
yield line_pad(line, 0, width - cell_length, style)
242240

243241
elif horizontal == "center":
244-
left_space = horizontal_excess_space // 2
242+
left_space = max(0, width - shape_width) // 2
245243
for cell_length, line in zip(line_lengths, lines):
246244
if cell_length == width:
247245
yield line

src/textual/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@
152152
_ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED = sys.version_info >= (3, 10, 0)
153153

154154
ComposeResult = Iterable[Widget]
155-
RenderResult = RenderableType
155+
RenderResult = "RenderableType | Visual | SupportsTextualize"
156+
"""Result of Widget.render()"""
156157

157158
AutopilotCallbackType: TypeAlias = (
158159
"Callable[[Pilot[object]], Coroutine[Any, Any, None]]"

src/textual/color.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,16 @@ class Color(NamedTuple):
167167
"""Alpha (opacity) component in range 0 to 1."""
168168
ansi: int | None = None
169169
"""ANSI color index. `-1` means default color. `None` if not an ANSI color."""
170+
auto: bool = False
171+
"""Is the color automatic? (automatic colors may be white or black, to provide maximum contrast)"""
170172

171173
@classmethod
172-
def from_rich_color(cls, rich_color: RichColor) -> Color:
174+
def automatic(cls, alpha_percentage: float = 100.0) -> Color:
175+
"""Create an automatic color."""
176+
return cls(0, 0, 0, alpha_percentage / 100.0, auto=True)
177+
178+
@classmethod
179+
def from_rich_color(cls, rich_color: RichColor | None) -> Color:
173180
"""Create a new color from Rich's Color class.
174181
175182
Args:
@@ -178,6 +185,8 @@ def from_rich_color(cls, rich_color: RichColor) -> Color:
178185
Returns:
179186
A new Color instance.
180187
"""
188+
if rich_color is None:
189+
return TRANSPARENT
181190
r, g, b = rich_color.get_truecolor()
182191
return cls(r, g, b)
183192

@@ -203,7 +212,7 @@ def inverse(self) -> Color:
203212
Returns:
204213
Inverse color.
205214
"""
206-
r, g, b, a, _ = self
215+
r, g, b, a, _, _ = self
207216
return Color(255 - r, 255 - g, 255 - b, a)
208217

209218
@property
@@ -214,14 +223,15 @@ def is_transparent(self) -> bool:
214223
@property
215224
def clamped(self) -> Color:
216225
"""A clamped color (this color with all values in expected range)."""
217-
r, g, b, a, _ = self
226+
r, g, b, a, ansi, auto = self
218227
_clamp = clamp
219228
color = Color(
220229
_clamp(r, 0, 255),
221230
_clamp(g, 0, 255),
222231
_clamp(b, 0, 255),
223232
_clamp(a, 0.0, 1.0),
224-
self.ansi,
233+
ansi,
234+
auto,
225235
)
226236
return color
227237

@@ -233,7 +243,7 @@ def rich_color(self) -> RichColor:
233243
Returns:
234244
A color object as used by Rich.
235245
"""
236-
r, g, b, _a, ansi = self
246+
r, g, b, _a, ansi, _ = self
237247
if ansi is not None:
238248
return RichColor.parse("default") if ansi < 0 else RichColor.from_ansi(ansi)
239249
return RichColor(
@@ -247,13 +257,13 @@ def normalized(self) -> tuple[float, float, float]:
247257
Returns:
248258
Normalized components.
249259
"""
250-
r, g, b, _a, _ = self
260+
r, g, b, _a, _, _ = self
251261
return (r / 255, g / 255, b / 255)
252262

253263
@property
254264
def rgb(self) -> tuple[int, int, int]:
255265
"""The red, green, and blue color components as a tuple of ints."""
256-
r, g, b, _, _ = self
266+
r, g, b, _, _, _ = self
257267
return (r, g, b)
258268

259269
@property
@@ -286,7 +296,7 @@ def hex(self) -> str:
286296
287297
For example, `"#46b3de"` for an RGB color, or `"#3342457f"` for a color with alpha.
288298
"""
289-
r, g, b, a, ansi = self.clamped
299+
r, g, b, a, ansi, _ = self.clamped
290300
if ansi is not None:
291301
return "ansi_default" if ansi == -1 else f"ansi_{ANSI_COLORS[ansi]}"
292302
return (
@@ -301,7 +311,7 @@ def hex6(self) -> str:
301311
302312
For example, `"#46b3de"`.
303313
"""
304-
r, g, b, _a, _ = self.clamped
314+
r, g, b, _a, _, _ = self.clamped
305315
return f"#{r:02X}{g:02X}{b:02X}"
306316

307317
@property
@@ -310,7 +320,12 @@ def css(self) -> str:
310320
311321
For example, `"rgb(10,20,30)"` for an RGB color, or `"rgb(50,70,80,0.5)"` for an RGBA color.
312322
"""
313-
r, g, b, a, ansi = self
323+
r, g, b, a, ansi, auto = self
324+
if auto:
325+
alpha_percentage = clamp(a, 0.0, 1.0) * 100.0
326+
if not alpha_percentage % 1:
327+
return f"auto {int(alpha_percentage)}%"
328+
return f"auto {alpha_percentage:.1f}%"
314329
if ansi is not None:
315330
return "ansi_default" if ansi == -1 else f"ansi_{ANSI_COLORS[ansi]}"
316331
return f"rgb({r},{g},{b})" if a == 1 else f"rgba({r},{g},{b},{a})"
@@ -322,17 +337,18 @@ def monochrome(self) -> Color:
322337
Returns:
323338
The monochrome (black and white) version of this color.
324339
"""
325-
r, g, b, a, _ = self
340+
r, g, b, a, _, _ = self
326341
gray = round(r * 0.2126 + g * 0.7152 + b * 0.0722)
327342
return Color(gray, gray, gray, a)
328343

329344
def __rich_repr__(self) -> rich.repr.Result:
330-
r, g, b, a, ansi = self
345+
r, g, b, a, ansi, auto = self
331346
yield r
332347
yield g
333348
yield b
334349
yield "a", a, 1.0
335350
yield "ansi", ansi, None
351+
yield "auto", auto, False
336352

337353
def with_alpha(self, alpha: float) -> Color:
338354
"""Create a new color with the given alpha.
@@ -343,7 +359,7 @@ def with_alpha(self, alpha: float) -> Color:
343359
Returns:
344360
A new color.
345361
"""
346-
r, g, b, _, _ = self
362+
r, g, b, _, _, _ = self
347363
return Color(r, g, b, alpha)
348364

349365
def multiply_alpha(self, alpha: float) -> Color:
@@ -357,7 +373,7 @@ def multiply_alpha(self, alpha: float) -> Color:
357373
"""
358374
if self.ansi is not None:
359375
return self
360-
r, g, b, a, _ = self
376+
r, g, b, a, _, _ = self
361377
return Color(r, g, b, a * alpha)
362378

363379
@lru_cache(maxsize=1024)
@@ -378,14 +394,16 @@ def blend(
378394
Returns:
379395
A new color.
380396
"""
397+
if destination.auto:
398+
destination = self.get_contrast_text(destination.a)
381399
if destination.ansi is not None:
382400
return destination
383401
if factor <= 0:
384402
return self
385403
elif factor >= 1:
386404
return destination
387-
r1, g1, b1, a1, _ = self
388-
r2, g2, b2, a2, _ = destination
405+
r1, g1, b1, a1, _, _ = self
406+
r2, g2, b2, a2, _, _ = destination
389407

390408
if alpha is None:
391409
new_alpha = a1 + (a2 - a1) * factor
@@ -412,10 +430,10 @@ def tint(self, color: Color) -> Color:
412430
New color
413431
"""
414432

415-
r1, g1, b1, a1, ansi1 = self
433+
r1, g1, b1, a1, ansi1, _ = self
416434
if ansi1 is not None:
417435
return self
418-
r2, g2, b2, a2, ansi2 = color
436+
r2, g2, b2, a2, ansi2, _ = color
419437
if ansi2 is not None:
420438
return self
421439
return Color(
@@ -551,7 +569,7 @@ def parse(cls, color_text: str | Color) -> Color:
551569
l = percentage_string_to_float(l)
552570
a = clamp(float(a), 0.0, 1.0)
553571
color = Color.from_hsl(h, s, l).with_alpha(a)
554-
else:
572+
else: # pragma: no-cover
555573
raise AssertionError( # pragma: no-cover
556574
"Can't get here if RE_COLOR matches"
557575
)

0 commit comments

Comments
 (0)