@@ -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