Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issues around component classes / (text) opacity #3415

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- `Pilot.click`/`Pilot.hover` can't use `Screen` as a selector https://github.com/Textualize/textual/issues/3395
- App exception when a `Tree` is initialized/mounted with `disabled=True` https://github.com/Textualize/textual/issues/3407
- `DOMNode.rich_style` didn't take text opacity into account when determining foreground color https://github.com/Textualize/textual/pull/3415
- `DOMNode.partial_rich_style` didn't take text opacity into account when determining foreground color https://github.com/Textualize/textual/pull/3415
- Fixed application freeze when pasting an emoji into an application on Windows https://github.com/Textualize/textual/issues/3178

### Added
Expand Down
55 changes: 44 additions & 11 deletions src/textual/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@

from abc import ABC, abstractmethod
from asyncio import CancelledError, Queue, Task, TimeoutError, wait, wait_for
from contextlib import contextmanager
from dataclasses import dataclass
from functools import total_ordering
from time import monotonic
from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, ClassVar
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
AsyncIterator,
ClassVar,
Generator,
)

import rich.repr
from rich.align import Align
Expand All @@ -26,8 +34,10 @@
from ._asyncio import create_task
from .binding import Binding, BindingType
from .containers import Horizontal, Vertical
from .css.styles import RenderStyles
from .events import Click, Mount
from .fuzzy import Matcher
from .message_pump import MessagePump
from .reactive import var
from .screen import ModalScreen, Screen
from .timer import Timer
Expand All @@ -49,6 +59,29 @@
]


@contextmanager
def _redirect_component_styles(
component_styles: RenderStyles, temporary_parent: MessagePump
) -> Generator[None, None, None]:
"""Temporarily move the virtual node of a component class in the DOM.

Args:
component_styles: The component styles whose node we want to redirect.
temporary_parent: The temporary parent for the redirected component styles.
"""
parent: MessagePump | None
if component_styles.node is not None:
parent = component_styles.node._parent
component_styles.node._parent = temporary_parent
else:
parent = None
try:
yield
finally:
if component_styles.node is not None:
component_styles.node._parent = parent


@dataclass
class Hit:
"""Holds the details of a single command search hit."""
Expand Down Expand Up @@ -522,9 +555,9 @@ def on_mount(self, _: Mount) -> None:
"""Capture the calling screen."""
self._calling_screen = self.app.screen_stack[-2]

match_style = self.get_component_rich_style(
"command-palette--highlight", partial=True
)
highlight_styles = self.get_component_styles("command-palette--highlight")
with _redirect_component_styles(highlight_styles, self.query_one(CommandList)):
match_style = highlight_styles.partial_rich_style

assert self._calling_screen is not None
self._providers = [
Expand Down Expand Up @@ -782,20 +815,20 @@ async def _gather_commands(self, search_value: str) -> None:
search_value: The value to search for.
"""

# Get a reference to the widget that we're going to drop the
# (display of) commands into.
command_list = self.query_one(CommandList)

# We'll potentially use the help text style a lot so let's grab it
# the once for use in the loop further down.
help_style = self._sans_background(
self.get_component_rich_style("command-palette--help-text")
)
help_text_styles = self.get_component_styles("command-palette--help-text")
with _redirect_component_styles(help_text_styles, command_list):
help_style = self._sans_background(help_text_styles.rich_style)

# The list to hold on to the commands we've gathered from the
# command providers.
gathered_commands: list[Command] = []

# Get a reference to the widget that we're going to drop the
# (display of) commands into.
command_list = self.query_one(CommandList)

# If there's just one option in the list, and it's the item that
# tells the user there were no matches, let's remove that. We're
# starting a new search so we don't want them thinking there's no
Expand Down
24 changes: 18 additions & 6 deletions src/textual/css/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,12 +627,24 @@ def partial_rich_style(self) -> Style:
Returns:
Rich Style object.
"""
style = Style(
color=(self.color.rich_color if self.has_rule("color") else None),
bgcolor=(
self.background.rich_color if self.has_rule("background") else None
),
)
if self.has_rule("color"):
# If there is text opacity, we need to get the background color from the
# parents to compute the correct final text color.
if self.has_rule("text_opacity") and self.text_opacity < 1:
reference_background = (
self.node._opacity_background_colors[1]
if self.node is not None
else self.background
)
color = (
reference_background + self.color.multiply_alpha(self.text_opacity)
).rich_color
else:
color = self.color.rich_color
else:
color = None
bgcolor = self.background.rich_color if self.has_rule("background") else None
style = Style(color=color, bgcolor=bgcolor)
style += self.text_style
return style

Expand Down
5 changes: 4 additions & 1 deletion src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ def rich_style(self) -> Style:

style = Style()
opacity = 1.0
text_opacity = 1.0

for node in reversed(self.ancestors_with_self):
styles = node.styles
Expand All @@ -786,8 +787,10 @@ def rich_style(self) -> Style:
background += styles.background.multiply_alpha(opacity)
else:
text_background = background
if styles.has_rule("text_opacity"):
text_opacity = styles.text_opacity
if styles.has_rule("color"):
color = styles.color
color = styles.color.multiply_alpha(text_opacity)
style += styles.text_style
if styles.has_rule("auto_color") and styles.auto_color:
color = text_background.get_contrast_text(color.a)
Expand Down
2 changes: 1 addition & 1 deletion src/textual/widgets/_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class Tab(Static):
}
Tab:disabled {
color: $text-disabled;
text-opacity: 50%;
text-opacity: 70%;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accounts for the now-correct calculation of the final text color when opacity is present.

}
Tab.-hidden {
display: none;
Expand Down
Loading