diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index 2e27db2ca9..499f53a50b 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -327,7 +327,7 @@ The `background: green` is only applied to the Button underneath the mouse curso Here are some other pseudo classes: - `:blur` Matches widgets which *do not* have input focus. -- `:dark` Matches widgets in dark mode (where `App.dark == True`). +- `:dark` Matches widgets in dark themes (where `App.theme.dark == True`). - `:disabled` Matches widgets which are in a disabled state. - `:enabled` Matches widgets which are in an enabled state. - `:even` Matches a widget at an evenly numbered position within its siblings. @@ -336,7 +336,7 @@ Here are some other pseudo classes: - `:focus` Matches widgets which have input focus. - `:inline` Matches widgets when the app is running in inline mode. - `:last-of-type` Matches a widget that is the last of its type amongst its siblings. -- `:light` Matches widgets in dark mode (where `App.dark == False`). +- `:light` Matches widgets in light themes (where `App.theme.dark == False`). - `:odd` Matches a widget at an oddly numbered position within its siblings. ## Combinators diff --git a/examples/theme_sandbox.py b/examples/theme_sandbox.py index 07513f08a6..9445c634ae 100644 --- a/examples/theme_sandbox.py +++ b/examples/theme_sandbox.py @@ -248,7 +248,7 @@ def watch_theme(self, theme_name: str) -> None: def compose(self) -> ComposeResult: with Grid(id="palette"): - theme = self.get_theme(self.theme) + theme = self.current_theme for variable, value in vars(theme).items(): if variable not in { "name", diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 0373136bba..e315716f30 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -90,8 +90,7 @@ def apply_css(self, text_area: TextArea) -> None: if self.base_style.color is None: self.base_style = Style(color="#f3f3f3", bgcolor=self.base_style.bgcolor) - app = text_area.app - app_theme = app.get_theme(app.theme) + app_theme = text_area.app.current_theme if self.base_style.bgcolor is None: self.base_style = Style( diff --git a/src/textual/app.py b/src/textual/app.py index 8e90ba5cc7..20f4aa6ed9 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -558,7 +558,9 @@ def __init__( This excludes the built-in themes.""" - ansi_theme = self.ansi_theme_dark if self.dark else self.ansi_theme_light + ansi_theme = ( + self.ansi_theme_dark if self.current_theme.dark else self.ansi_theme_light + ) self.set_reactive(App.ansi_color, ansi_color) self._filters: list[LineFilter] = [ ANSIToTruecolor(ansi_theme, enabled=not ansi_color) @@ -764,8 +766,8 @@ def __init__( perform work after the app has resumed. """ - self.set_class(self.dark, "-dark-mode") - self.set_class(not self.dark, "-light-mode") + self.set_class(self.current_theme.dark, "-dark-mode") + self.set_class(not self.current_theme.dark, "-light-mode") self.animation_level: AnimationLevel = constants.TEXTUAL_ANIMATIONS """Determines what type of animations the app will display. @@ -1183,11 +1185,7 @@ def get_css_variables(self) -> dict[str, str]: Returns: A mapping of variable name to value. """ - theme = self.get_theme(self.theme) - if theme is None: - # This should never happen thanks to the `App.theme` validator. - return {} - + theme = self.current_theme # Build the Textual color system from the theme. # This will contain $secondary, $primary, $background, etc. variables = theme.to_color_system().generate() @@ -1265,28 +1263,13 @@ def _watch_theme(self, theme_name: str) -> None: self.call_next(self.refresh_css) self.call_next(self.theme_changed_signal.publish, theme) - def watch_dark(self, dark: bool) -> None: - """Watches the dark bool. - - This method handles the transition between light and dark mode when you - change the [dark][textual.app.App.dark] attribute. - """ - self.set_class(dark, "-dark-mode", update=False) - self.set_class(not dark, "-light-mode", update=False) - self._refresh_truecolor_filter(self.ansi_theme) - self.call_next(self.refresh_css) - def watch_ansi_theme_dark(self, theme: TerminalTheme) -> None: - app_theme = self.get_theme(self.theme) - assert app_theme is not None # validated by _validate_theme - if app_theme.dark: + if self.current_theme.dark: self._refresh_truecolor_filter(theme) self.call_next(self.refresh_css) def watch_ansi_theme_light(self, theme: TerminalTheme) -> None: - app_theme = self.get_theme(self.theme) - assert app_theme is not None # validated by _validate_theme - if not app_theme.dark: + if not self.current_theme.dark: self._refresh_truecolor_filter(theme) self.call_next(self.refresh_css) @@ -1297,9 +1280,9 @@ def ansi_theme(self) -> TerminalTheme: Defines how colors defined as ANSI (e.g. `magenta`) inside Rich renderables are mapped to hex codes. """ - app_theme = self.get_theme(self.theme) - assert app_theme is not None # validated by _validate_theme - return self.ansi_theme_dark if app_theme.dark else self.ansi_theme_light + return ( + self.ansi_theme_dark if self.current_theme.dark else self.ansi_theme_light + ) def _refresh_truecolor_filter(self, theme: TerminalTheme) -> None: """Update the ANSI to Truecolor filter, if available, with a new theme mapping. diff --git a/src/textual/dom.py b/src/textual/dom.py index 89be69a9fb..d0b9bb4723 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -237,7 +237,7 @@ def set_reactive( Example: ```python - self.set_reactive(App.dark_mode, True) + self.set_reactive(App.theme, "textual-light") ``` Args: @@ -247,15 +247,14 @@ def set_reactive( Raises: AttributeError: If the first argument is not a reactive. """ + name = reactive.name if not isinstance(reactive, Reactive): - raise TypeError( - "A Reactive class is required; for example: MyApp.dark_mode" - ) - if reactive.name not in self._reactives: + raise TypeError("A Reactive class is required; for example: MyApp.theme") + if name not in self._reactives: raise AttributeError( - "No reactive called {name!r}; Have you called super().__init__(...) in the {self.__class__.__name__} constructor?" + f"No reactive called {name!r}; Have you called super().__init__(...) in the {self.__class__.__name__} constructor?" ) - setattr(self, f"_reactive_{reactive.name}", value) + setattr(self, f"_reactive_{name}", value) def mutate_reactive(self, reactive: Reactive[ReactiveType]) -> None: """Force an update to a mutable reactive. @@ -1218,11 +1217,11 @@ def watch( Example: ```python - def on_dark_change(old_value:bool, new_value:bool) -> None: - # Called when app.dark changes. - print("App.dark went from {old_value} to {new_value}") + def on_theme_change(old_value:str, new_value:str) -> None: + # Called when app.theme changes. + print(f"App.theme went from {old_value} to {new_value}") - self.watch(self.app, "dark", self.on_dark_change, init=False) + self.watch(self.app, "theme", self.on_theme_change, init=False) ``` Args: diff --git a/src/textual/widget.py b/src/textual/widget.py index 585318919c..48427282c8 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -375,8 +375,8 @@ class Widget(DOMNode): "can-focus": lambda widget: widget.can_focus, "disabled": lambda widget: widget.is_disabled, "enabled": lambda widget: not widget.is_disabled, - "dark": lambda widget: widget.app.dark, - "light": lambda widget: not widget.app.dark, + "dark": lambda widget: widget.app.current_theme.dark, + "light": lambda widget: not widget.app.current_theme.dark, "focus-within": lambda widget: widget.has_focus_within, "inline": lambda widget: widget.app.is_inline, "ansi": lambda widget: widget.app.ansi_color, diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 694c14944d..ee4ddd00b7 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -619,10 +619,9 @@ def __init__(self, markdown: Markdown, code: str, lexer: str) -> None: super().__init__(markdown) self.code = code self.lexer = lexer - app_theme = self.app.get_theme(self.app.theme) self.theme = ( self._markdown.code_dark_theme - if app_theme.dark + if self.app.current_theme.dark else self._markdown.code_light_theme ) @@ -642,10 +641,9 @@ def _on_mount(self, _: Mount) -> None: def _retheme(self) -> None: """Rerender when the theme changes.""" - app_theme = self.app.get_theme(self.app.theme) self.theme = ( self._markdown.code_dark_theme - if app_theme.dark + if self.app.current_theme.dark else self._markdown.code_light_theme ) self.get_child_by_type(Static).update(self._block()) @@ -807,13 +805,13 @@ async def _on_mount(self, _: Mount) -> None: def _watch_code_dark_theme(self) -> None: """React to the dark theme being changed.""" - if self.app.dark: + if self.app.current_theme.dark: for block in self.query(MarkdownFence): block._retheme() def _watch_code_light_theme(self) -> None: """React to the light theme being changed.""" - if not self.app.dark: + if not self.app.current_theme.dark: for block in self.query(MarkdownFence): block._retheme() diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 44fd681554..63d2d95a61 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -745,7 +745,7 @@ def _watch_theme(self, theme: str) -> None: if padding is applied, the colors match.""" self._set_theme(theme) - def _app_dark_toggled(self) -> None: + def _app_theme_changed(self) -> None: self._set_theme(self._theme.name) def _set_theme(self, theme: str) -> None: @@ -1521,8 +1521,8 @@ def gutter_width(self) -> int: return gutter_width def _on_mount(self, event: events.Mount) -> None: - # When `app.dark` is toggled, reset the theme (since it caches values). - self.watch(self.app, "dark", self._app_dark_toggled, init=False) + # When `app.theme` reactive is changed, reset the theme to clear cached styles. + self.watch(self.app, "theme", self._app_theme_changed, init=False) self.blink_timer = self.set_interval( 0.5,