mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
adds demo
This commit is contained in:
6
src/textual/__main__.py
Normal file
6
src/textual/__main__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .demo import DemoApp
|
||||
|
||||
|
||||
app = DemoApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
89
src/textual/demo.css
Normal 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
90
src/textual/demo.py
Normal 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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user