adds demo

This commit is contained in:
Will McGugan
2022-10-19 15:03:05 +01:00
parent 46a885cea7
commit ca16b2d659
12 changed files with 224 additions and 28 deletions

6
src/textual/__main__.py Normal file
View File

@@ -0,0 +1,6 @@
from .demo import DemoApp
app = DemoApp()
if __name__ == "__main__":
app.run()

View File

@@ -86,6 +86,7 @@ class SimpleAnimation(Animation):
assert isinstance(
self.end_value, (int, float)
), f"`end_value` must be float, not {self.end_value!r}"
if self.end_value > self.start_value:
eased_factor = self.easing(factor)
value = (

View File

@@ -139,18 +139,18 @@ class App(Generic[ReturnType], DOMNode):
"""
SCREENS: dict[str, Screen] = {}
_BASE_PATH: str | None = None
CSS_PATH: CSSPathType = None
TITLE: str | None = None
SUB_TITLE: str | None = None
title: Reactive[str] = Reactive("Textual")
title: Reactive[str] = Reactive("")
sub_title: Reactive[str] = Reactive("")
dark: Reactive[bool] = Reactive(True)
def __init__(
self,
driver_class: Type[Driver] | None = None,
title: str | None = None,
css_path: CSSPathType = None,
watch_css: bool = False,
):
@@ -189,10 +189,10 @@ class App(Generic[ReturnType], DOMNode):
self._animator = Animator(self)
self._animate = self._animator.bind(self)
self.mouse_position = Offset(0, 0)
if title is None:
self.title = f"{self.__class__.__name__}"
else:
self.title = title
self.title = (
self.TITLE if self.TITLE is not None else f"{self.__class__.__name__}"
)
self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else ""
self._logger = Logger(self._log)

View File

@@ -341,7 +341,9 @@ class Color(NamedTuple):
r, g, b, _ = self
return Color(r, g, b, alpha)
def blend(self, destination: Color, factor: float, alpha: float = 1) -> Color:
def blend(
self, destination: Color, factor: float, alpha: float | None = None
) -> Color:
"""Generate a new color between two colors.
Args:
@@ -353,21 +355,21 @@ class Color(NamedTuple):
Color: A new color.
"""
if factor == 0:
return self
return self if alpha is None else self.with_alpha(alpha)
elif factor == 1:
return destination
r1, g1, b1, _ = self
r2, g2, b2, _ = destination
return destination if alpha is None else destination.with_alpha(alpha)
r1, g1, b1, a1 = self
r2, g2, b2, a2 = destination
return Color(
int(r1 + (r2 - r1) * factor),
int(g1 + (g2 - g1) * factor),
int(b1 + (b2 - b1) * factor),
alpha,
a1 + (a2 - a1) * factor if alpha is None else alpha,
)
def __add__(self, other: object) -> Color:
if isinstance(other, Color):
new_color = self.blend(other, other.a)
new_color = self.blend(other, other.a, alpha=1.0)
return new_color
return NotImplemented

View File

@@ -856,6 +856,7 @@ class ColorProperty:
elif isinstance(color, Color):
if obj.set_rule(self.name, color):
obj.refresh(children=self._is_background)
elif isinstance(color, str):
alpha = 1.0
parsed_color = Color(255, 255, 255)

View File

@@ -58,10 +58,13 @@ class ScalarAnimation(Animation):
setattr(self.styles, self.attribute, self.final_value)
return True
offset = self.start + (self.destination - self.start) * eased_factor
current = self.styles._rules[self.attribute]
if current != offset:
setattr(self.styles, f"{self.attribute}", offset)
if hasattr(self.start, "blend"):
value = self.start.blend(self.destination, eased_factor)
else:
value = self.start + (self.destination - self.start) * eased_factor
current = self.styles._rules.get(self.attribute)
if current != value:
setattr(self.styles, f"{self.attribute}", value)
return False

View File

@@ -644,9 +644,6 @@ class Styles(StylesBase):
easing: EasingFunction,
on_complete: CallbackType | None = None,
) -> ScalarAnimation | None:
# from ..widget import Widget
# node = self.node
# assert isinstance(self.node, Widget)
if isinstance(value, ScalarOffset):
return ScalarAnimation(
self.node,

89
src/textual/demo.css Normal file
View File

@@ -0,0 +1,89 @@
* {
transition:background 300ms linear, color 300ms linear;
}
Screen {
layers: base overlay notes;
overflow: hidden;
}
Sidebar {
width: 30;
background: $panel;
transition: offset 500ms in_out_cubic;
layer: overlay;
}
Sidebar.-hidden {
offset-x: -100%;
}
Sidebar Title {
background: $boost;
color: $text;
padding: 2 4;
border-right: vkey $background;
dock: top;
text-align: center;
}
Body {
height: 100%;
overflow-y: auto;
align: center middle;
background: $surface;
}
Welcome {
background: $panel;
height: auto;
margin: 1 2;
padding: 1 2;
max-width: 60;
border: wide $accent;
}
#dark-switcher {
width: 100%;
height: 5;
align: center middle;
}
DarkSwitch {
background: $panel-lighten-1;
dock: bottom;
height: auto;
border-right: vkey $background;
}
DarkSwitch .label {
padding: 1 2;
color: $text-muted;
}
Screen > Container {
height: 100%;
overflow: hidden;
}
TextLog {
background: $surface;
color: rgba(255,255,255,0.9);
height: 50vh;
dock: bottom;
layer: notes;
offset-y: 0;
transition: offset 200ms in_out_cubic;
}
TextLog.-hidden {
offset-y: 100%;
transition: offset 200ms in_out_cubic;
}

90
src/textual/demo.py Normal file
View File

@@ -0,0 +1,90 @@
from rich.console import RenderableType
from rich.markdown import Markdown
from rich.text import Text
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal
from textual.reactive import reactive, watch
from textual.widgets import Header, Footer, Static, Button, Checkbox, TextLog
WELCOME_MD = """
## Textual Demo
Welcome to the Textual demo!
-
"""
class Body(Container):
pass
class Title(Static):
pass
class DarkSwitch(Horizontal):
def compose(self) -> ComposeResult:
yield Checkbox(value=self.app.dark)
yield Static("Dark mode", classes="label")
def on_mount(self) -> None:
watch(self.app, "dark", self.on_dark_change)
def on_dark_change(self, dark: bool) -> None:
self.query_one(Checkbox).value = dark
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
self.app.dark = event.value
class Welcome(Container):
def compose(self) -> ComposeResult:
yield Static(Markdown(WELCOME_MD))
class Sidebar(Container):
def compose(self) -> ComposeResult:
yield Title("Textual Demo")
yield Container()
yield DarkSwitch()
class DemoApp(App):
CSS_PATH = "demo.css"
TITLE = "Textual Demo"
BINDINGS = [
("s", "app.toggle_class('Sidebar', '-hidden')", "Sidebar"),
("d", "app.toggle_dark", "Toggle Dark mode"),
("n", "app.toggle_class('TextLog', '-hidden')", "Notes"),
]
show_sidebar = reactive(False)
def add_note(self, renderable: RenderableType) -> None:
self.query_one(TextLog).write(renderable)
def on_mount(self) -> None:
self.add_note("[b]Textual Nodes")
def compose(self) -> ComposeResult:
yield Container(
Sidebar(),
Header(),
TextLog(classes="-hidden", wrap=False, highlight=True, markup=True),
Body(Welcome()),
)
yield Footer()
def on_dark_switch_toggle(self) -> None:
self.dark = not self.dark
app = DemoApp()
if __name__ == "__main__":
app.run()

View File

@@ -128,7 +128,7 @@ class ColorSystem:
boost = self.boost or background.get_contrast_text(1.0).with_alpha(0.04)
if self.panel is None:
panel = surface.blend(primary, 0.1)
panel = surface.blend(primary, 0.1, alpha=1)
if dark:
panel += boost
else:
@@ -154,7 +154,7 @@ class ColorSystem:
yield (f"{label}{'-' + str(abs(n)) if n else ''}"), n * luminosity_step
# Color names and color
COLORS = [
COLORS: list[tuple[str, Color]] = [
("primary", primary),
("secondary", secondary),
("primary-background", primary),
@@ -178,9 +178,9 @@ class ColorSystem:
spread = luminosity_spread
for shade_name, luminosity_delta in luminosity_range(spread):
if is_dark_shade:
dark_background = background.blend(color, 0.15)
dark_background = background.blend(color, 0.15, alpha=1.0)
shade_color = dark_background.blend(
WHITE, spread + luminosity_delta
WHITE, spread + luminosity_delta, alpha=1.0
).clamped
colors[f"{name}{shade_name}"] = shade_color.hex
else:

View File

@@ -58,8 +58,8 @@ class HeaderTitle(Widget):
}
"""
text: Reactive[str] = Reactive("Hello World")
sub_text = Reactive("Test")
text: Reactive[str] = Reactive("")
sub_text = Reactive("")
def render(self) -> Text:
text = Text(self.text, no_wrap=True, overflow="ellipsis")

View File

@@ -30,6 +30,7 @@ class TextLog(ScrollView, can_focus=True):
min_width: var[int] = var(78)
wrap: var[bool] = var(False)
highlight: var[bool] = var(False)
markup: var[bool] = var(False)
def __init__(
self,
@@ -38,6 +39,7 @@ class TextLog(ScrollView, can_focus=True):
min_width: int = 78,
wrap: bool = False,
highlight: bool = False,
markup: bool = False,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
@@ -51,6 +53,7 @@ class TextLog(ScrollView, can_focus=True):
self.min_width = min_width
self.wrap = wrap
self.highlight = highlight
self.markup = markup
self.highlighter = ReprHighlighter()
def _on_styles_updated(self) -> None:
@@ -68,6 +71,8 @@ class TextLog(ScrollView, can_focus=True):
renderable = Pretty(content)
else:
if isinstance(content, str):
if self.markup:
content = Text.from_markup(content)
if self.highlight:
renderable = self.highlighter(content)
else:
@@ -102,7 +107,9 @@ class TextLog(ScrollView, can_focus=True):
def render_line(self, y: int) -> list[Segment]:
scroll_x, scroll_y = self.scroll_offset
return self._render_line(scroll_y + y, scroll_x, self.size.width)
line = self._render_line(scroll_y + y, scroll_x, self.size.width)
line = list(Segment.apply_style(line, post_style=self.rich_style))
return line
def render_lines(self, crop: Region) -> Lines:
"""Render the widget in to lines.