Skip to content

Commit

Permalink
new: Allow multiple simultaneous CSS themes.
Browse files Browse the repository at this point in the history
  • Loading branch information
dfrtz authored Sep 9, 2023
1 parent 96fe38a commit 710b18b
Show file tree
Hide file tree
Showing 11 changed files with 707 additions and 16 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ exception: Python3.10 minimum requirement.
- Enhanced testing support
- Parallel tests via python-xdist
- Custom testing arguments, such as updating snapshots on failures
- Multiple theme support
- Swap CSS themes live
- Apply multiple CSS themes simultaneously


## Getting Started
Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Core features and functionality.
- [x] Custom themes
* [x] Named themes by CSS paths
* [ ] Named themes by raw CSS
* [ ] Multiple simultaneous themes
* [x] Multiple simultaneous themes
- [ ] HTML templates for layouts
- [x] Testing
* [x] Parallel processing support (python-xdist)
Expand Down
48 changes: 33 additions & 15 deletions textology/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def __init__( # pylint: disable=too-many-arguments
driver_class: type[Driver] | None = None,
css_path: CSSPathType | None = None,
watch_css: bool = False,
css_theme: str | None = None,
css_theme: str | list[str] | None = None,
css_themes: dict[str, list[CSSPathType]] | None = None,
logger: logging.Logger | None = None,
) -> None:
Expand Down Expand Up @@ -132,15 +132,18 @@ def __init__( # pylint: disable=too-many-arguments
css_path = css_path or self.CSS_PATH
if css_path and not isinstance(css_path, list):
css_path = [css_path]
self.css_theme = css_theme
if css_theme and not isinstance(css_theme, list):
css_theme = [css_theme]
self._css_theme: list[str] = css_theme
self.css_themes = css_themes or self.CSS_THEMES or {}
for css_theme_name, css_theme_paths in self.css_themes.items():
self.css_themes[css_theme_name] = [
_make_path_object_relative(css_theme_path, self) for css_theme_path in css_theme_paths
]
if self.css_theme and self.css_theme in self.css_themes:
css_path = css_path or []
css_path.extend(self.css_themes[self.css_theme])
css_path = css_path or []
for theme in self._css_theme or []:
if theme in self.css_themes:
css_path.extend(self.css_themes[theme])

layout = layout or Container(
Location(id=_DEFAULT_URL_ID),
Expand Down Expand Up @@ -188,30 +191,40 @@ def apply_update(
else:
super().apply_update(observer_id, component, component_id, component_property, value)

def apply_theme(self, theme: str | None = None) -> None:
def apply_theme(self, theme: str | list[str] | None = None) -> None:
"""Load a CSS theme.
Themes are applied in addition to base "css_path" values, rather than in place of.
Args:
theme: Name of the CSS theme to load from "css_themes".
theme: Name(s) of the CSS theme(s) to load from "css_themes".
All other stylesheets not in the base CSS configuration will be unloaded.
"""
if theme and not isinstance(theme, list):
theme = [theme]
css_paths = self.css_path
stylesheet = self.stylesheet.copy()

if self.css_theme and self.css_theme in self.css_themes:
for css_theme_path in self.css_themes[self.css_theme]:
# Cleanup old themes first.
for css_theme in self._css_theme or []:
if css_theme not in self.css_themes:
continue
for css_theme_path in self.css_themes[css_theme]:
if css_theme_path in css_paths:
css_paths.remove(css_theme_path)
if stylesheet.has_source(css_theme_path):
stylesheet.source.pop(str(css_theme_path))
self.log.info(f"Removed CSS theme: {self.css_theme}")
self.css_theme = None
if theme and theme not in self.css_themes:
self.log.error(f"CSS Theme not found: {theme}")
for css_theme_path in self.css_themes.get(theme, []):
css_paths.append(css_theme_path)

# Apply new themes second.
for css_theme in theme or []:
if css_theme not in self.css_themes:
self.log.error(f"CSS Theme not found: {css_theme}")
continue
for css_theme_path in self.css_themes.get(css_theme, []):
css_paths.append(css_theme_path)

# Force reload of CSS and layouts last.
self.css_path = css_paths
try:
stylesheet.read_all(css_paths)
Expand All @@ -221,7 +234,7 @@ def apply_theme(self, theme: str | None = None) -> None:
self._css_has_errors = True
self.log.error(error)
else:
self.css_theme = theme
self._css_theme = theme
self._css_has_errors = False
self.stylesheet = stylesheet
self.refresh_css()
Expand All @@ -240,6 +253,11 @@ def back(self) -> int:
"""
return self.location.back()

@property
def css_theme(self) -> list[str] | None:
"""Provide the CSS themes currently applied on top of the base CSS."""
return self._css_theme

@property
def document(self) -> Screen | None:
"""Provide the base screen of the application, representing the main visible "document" in the window."""
Expand Down
3 changes: 3 additions & 0 deletions textology/test/basic_themed_app-green_btn.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#btn {
background: green;
}
3 changes: 3 additions & 0 deletions textology/test/basic_themed_app-white_border.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#btn {
border: tall white;
}
26 changes: 26 additions & 0 deletions textology/test/basic_themed_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Basic themed application for unit tests."""

from textual.app import ComposeResult

from textology import apps
from textology import widgets


class BasicThemedApp(apps.ExtendedApp):
"""Simple themed application with basic elements for testing interactions and snapshots."""

CSS_THEMES = {
"green": [
"basic_themed_app-green_btn.css",
],
"white": [
"basic_themed_app-white_border.css",
],
}

def compose(self) -> ComposeResult:
"""Provide basic layout."""
yield widgets.Button("Click me!", id="btn")


app = BasicThemedApp()
153 changes: 153 additions & 0 deletions textology/test/snapshots/test_themed_app_green.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 710b18b

Please sign in to comment.