mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
demo updates
This commit is contained in:
@@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added `can_focus` and `can_focus_children` parameters to scrollable container types. https://github.com/Textualize/textual/pull/5226
|
||||
- Added `textual.lazy.Reveal` https://github.com/Textualize/textual/pull/5226
|
||||
- Added `Screen.action_blur` https://github.com/Textualize/textual/pull/5226
|
||||
- `Click` events can now be used with the on decorator to match the initially clicked widget
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ class XTermParser(Parser[Message]):
|
||||
event_class = events.MouseDown if state == "M" else events.MouseUp
|
||||
|
||||
event = event_class(
|
||||
None,
|
||||
x,
|
||||
y,
|
||||
delta_x,
|
||||
|
||||
@@ -3702,7 +3702,9 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self.get_widget_at(event.x, event.y)[0]
|
||||
is self._mouse_down_widget
|
||||
):
|
||||
click_event = events.Click.from_event(event)
|
||||
click_event = events.Click.from_event(
|
||||
self._mouse_down_widget, event
|
||||
)
|
||||
self.screen._forward_event(click_event)
|
||||
except NoWidget:
|
||||
pass
|
||||
|
||||
@@ -15,6 +15,14 @@ class DemoApp(App):
|
||||
align: center top;
|
||||
&>*{ max-width: 100; }
|
||||
}
|
||||
Screen .-maximized {
|
||||
margin: 1 2;
|
||||
max-width: 100%;
|
||||
&.column { margin: 1 2; padding: 1 2; }
|
||||
&.column > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
MODES = {
|
||||
@@ -48,4 +56,30 @@ class DemoApp(App):
|
||||
"Screenshot",
|
||||
tooltip="Save an SVG 'screenshot' of the current screen",
|
||||
),
|
||||
Binding(
|
||||
"ctrl+m",
|
||||
"app.maximize",
|
||||
"Maximize",
|
||||
tooltip="Maximized the focused widget (if possible)",
|
||||
),
|
||||
]
|
||||
|
||||
def action_maximize(self) -> None:
|
||||
if self.screen.focused is None:
|
||||
self.notify(
|
||||
"Nothing to be maximized (try pressing [b]tab[/b])",
|
||||
title="Maximize",
|
||||
severity="warning",
|
||||
)
|
||||
else:
|
||||
if self.screen.maximize(self.screen.focused):
|
||||
self.notify(
|
||||
"You are now in the maximized view. Press [b]escape[/b] to return.",
|
||||
title="Maximize",
|
||||
)
|
||||
else:
|
||||
self.notify(
|
||||
"This widget may not be maximized.",
|
||||
title="Maximize",
|
||||
severity="warning",
|
||||
)
|
||||
|
||||
@@ -145,7 +145,7 @@ class StarCount(Vertical):
|
||||
color: $text-warning;
|
||||
#stars { align: center top; }
|
||||
#forks { align: right top; }
|
||||
Label { text-style: bold; }
|
||||
Label { text-style: bold; color: $foreground; }
|
||||
LoadingIndicator { background: transparent !important; }
|
||||
Digits { width: auto; margin-right: 1; }
|
||||
Label { margin-right: 1; }
|
||||
|
||||
@@ -8,13 +8,14 @@ from rich.syntax import Syntax
|
||||
from rich.table import Table
|
||||
from rich.traceback import Traceback
|
||||
|
||||
from textual import containers, lazy
|
||||
from textual import containers, events, lazy, on
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.demo.data import COUNTRIES
|
||||
from textual.demo.page import PageScreen
|
||||
from textual.reactive import reactive, var
|
||||
from textual.suggester import SuggestFromList
|
||||
from textual.theme import BUILTIN_THEMES
|
||||
from textual.widgets import (
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -33,6 +34,7 @@ from textual.widgets import (
|
||||
RadioSet,
|
||||
RichLog,
|
||||
Sparkline,
|
||||
Switch,
|
||||
TabbedContent,
|
||||
)
|
||||
|
||||
@@ -49,6 +51,7 @@ The following list is *not* exhaustive…
|
||||
class Buttons(containers.VerticalGroup):
|
||||
"""Buttons demo."""
|
||||
|
||||
ALLOW_MAXIMIZE = True
|
||||
DEFAULT_CLASSES = "column"
|
||||
DEFAULT_CSS = """
|
||||
Buttons {
|
||||
@@ -179,6 +182,7 @@ Cells may be individually styled, and may include Rich renderables.
|
||||
class Inputs(containers.VerticalGroup):
|
||||
"""Demonstrates Inputs."""
|
||||
|
||||
ALLOW_MAXIMIZE = True
|
||||
DEFAULT_CLASSES = "column"
|
||||
INPUTS_MD = """\
|
||||
## Inputs and MaskedInputs
|
||||
@@ -234,6 +238,7 @@ Build for intuitive and user-friendly forms.
|
||||
class ListViews(containers.VerticalGroup):
|
||||
"""Demonstrates List Views and Option Lists."""
|
||||
|
||||
ALLOW_MAXIMIZE = True
|
||||
DEFAULT_CLASSES = "column"
|
||||
LISTS_MD = """\
|
||||
## List Views and Option Lists
|
||||
@@ -435,6 +440,59 @@ For detailed graphs, see [textual-plotext](https://github.com/Textualize/textual
|
||||
self.data = [abs(sin(x / 3.14)) for x in range(offset, offset + 360 * 6, 20)]
|
||||
|
||||
|
||||
class Switches(containers.VerticalGroup):
|
||||
"""Demonstrate the Switch widget."""
|
||||
|
||||
ALLOW_MAXIMIZE = True
|
||||
DEFAULT_CLASSES = "column"
|
||||
SWITCHES_MD = """\
|
||||
## Switches
|
||||
|
||||
Functionally almost identical to a Checkbox, but more displays more prominently in the UI.
|
||||
"""
|
||||
DEFAULT_CSS = """\
|
||||
Switches {
|
||||
Label {
|
||||
padding: 1;
|
||||
&:hover {text-style:underline; }
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Markdown(self.SWITCHES_MD)
|
||||
with containers.ItemGrid(min_column_width=32):
|
||||
for theme in BUILTIN_THEMES:
|
||||
with containers.HorizontalGroup():
|
||||
yield Switch(id=theme)
|
||||
yield Label(theme, name=theme)
|
||||
|
||||
@on(events.Click, "Label")
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
"""Make the label toggle the switch."""
|
||||
# TODO: Add a dedicated form label
|
||||
event.stop()
|
||||
if event.widget is not None:
|
||||
self.query_one(f"#{event.widget.name}", Switch).toggle()
|
||||
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
# Don't issue more Changed events
|
||||
with self.prevent(Switch.Changed):
|
||||
# Reset all other switches
|
||||
for switch in self.query("Switch").results(Switch):
|
||||
if switch.id != event.switch.id:
|
||||
switch.value = False
|
||||
assert event.switch.id is not None
|
||||
theme_id = event.switch.id
|
||||
|
||||
def switch_theme() -> None:
|
||||
"""Callback to switch the theme."""
|
||||
self.app.theme = theme_id
|
||||
|
||||
# Call after a short delay, so we see the Switch animation
|
||||
self.set_timer(0.3, switch_theme)
|
||||
|
||||
|
||||
class WidgetsScreen(PageScreen):
|
||||
"""The Widgets screen"""
|
||||
|
||||
@@ -467,4 +525,5 @@ class WidgetsScreen(PageScreen):
|
||||
yield ListViews()
|
||||
yield Logs()
|
||||
yield Sparklines()
|
||||
yield Switches()
|
||||
yield Footer()
|
||||
|
||||
@@ -267,8 +267,6 @@ class LinuxDriver(Driver):
|
||||
self.write("\x1b[?25l") # Hide cursor
|
||||
self.write("\x1b[?1004h") # Enable FocusIn/FocusOut.
|
||||
self.write("\x1b[>1u") # https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
||||
# Disambiguate escape codes https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
|
||||
self.write("\x1b[=1;u")
|
||||
|
||||
self.flush()
|
||||
self._key_thread = Thread(target=self._run_input_thread)
|
||||
|
||||
@@ -327,6 +327,7 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
- [ ] Verbose
|
||||
|
||||
Args:
|
||||
widget: The widget under the mouse.
|
||||
x: The relative x coordinate.
|
||||
y: The relative y coordinate.
|
||||
delta_x: Change in x since the last message.
|
||||
@@ -341,6 +342,7 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
"""
|
||||
|
||||
__slots__ = [
|
||||
"widget",
|
||||
"x",
|
||||
"y",
|
||||
"delta_x",
|
||||
@@ -356,6 +358,7 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
widget: Widget | None,
|
||||
x: int,
|
||||
y: int,
|
||||
delta_x: int,
|
||||
@@ -369,6 +372,8 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
style: Style | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.widget: Widget | None = widget
|
||||
"""The widget under the mouse at the time of a click."""
|
||||
self.x = x
|
||||
"""The relative x coordinate."""
|
||||
self.y = y
|
||||
@@ -392,8 +397,11 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
self._style = style or Style()
|
||||
|
||||
@classmethod
|
||||
def from_event(cls: Type[MouseEventT], event: MouseEvent) -> MouseEventT:
|
||||
def from_event(
|
||||
cls: Type[MouseEventT], widget: Widget, event: MouseEvent
|
||||
) -> MouseEventT:
|
||||
new_event = cls(
|
||||
widget,
|
||||
event.x,
|
||||
event.y,
|
||||
event.delta_x,
|
||||
@@ -409,6 +417,7 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
return new_event
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield self.widget
|
||||
yield "x", self.x
|
||||
yield "y", self.y
|
||||
yield "delta_x", self.delta_x, 0
|
||||
@@ -422,6 +431,10 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
yield "meta", self.meta, False
|
||||
yield "ctrl", self.ctrl, False
|
||||
|
||||
@property
|
||||
def control(self) -> Widget | None:
|
||||
return self.widget
|
||||
|
||||
@property
|
||||
def offset(self) -> Offset:
|
||||
"""The mouse coordinate as an offset.
|
||||
@@ -478,6 +491,7 @@ class MouseEvent(InputEvent, bubble=True):
|
||||
|
||||
def _apply_offset(self, x: int, y: int) -> MouseEvent:
|
||||
return self.__class__(
|
||||
self.widget,
|
||||
x=self.x + x,
|
||||
y=self.y + y,
|
||||
delta_x=self.delta_x,
|
||||
|
||||
@@ -725,6 +725,8 @@ class MessagePump(metaclass=_MessagePumpMeta):
|
||||
continue
|
||||
for attribute, selector in selectors.items():
|
||||
node = getattr(message, attribute)
|
||||
if node is None:
|
||||
break
|
||||
if not isinstance(node, Widget):
|
||||
raise OnNoWidget(
|
||||
f"on decorator can't match against {attribute!r} as it is not a widget."
|
||||
|
||||
@@ -15,8 +15,10 @@ def blend_colors(color1: Color, color2: Color, ratio: float) -> Color:
|
||||
Returns:
|
||||
A Color representing the blending of the two supplied colors.
|
||||
"""
|
||||
assert color1.triplet is not None
|
||||
assert color2.triplet is not None
|
||||
# assert color1.triplet is not None
|
||||
# assert color2.triplet is not None
|
||||
if color1.triplet is None or color2.triplet is None:
|
||||
return color2
|
||||
r1, g1, b1 = color1.triplet
|
||||
r2, g2, b2 = color2.triplet
|
||||
|
||||
|
||||
@@ -746,12 +746,15 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
"""
|
||||
return self._move_focus(-1, selector)
|
||||
|
||||
def maximize(self, widget: Widget, container: bool = True) -> None:
|
||||
def maximize(self, widget: Widget, container: bool = True) -> bool:
|
||||
"""Maximize a widget, so it fills the screen.
|
||||
|
||||
Args:
|
||||
widget: Widget to maximize.
|
||||
container: If one of the widgets ancestors is a maximizeable widget, maximize that instead.
|
||||
|
||||
Returns:
|
||||
`True` if the widget was maximized, otherwise `False`.
|
||||
"""
|
||||
if widget.allow_maximize:
|
||||
if container:
|
||||
@@ -761,9 +764,10 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
break
|
||||
if maximize_widget.allow_maximize:
|
||||
self.maximized = maximize_widget
|
||||
return
|
||||
return True
|
||||
|
||||
self.maximized = widget
|
||||
return False
|
||||
|
||||
def minimize(self) -> None:
|
||||
"""Restore any maximized widget to normal state."""
|
||||
@@ -1371,6 +1375,7 @@ class Screen(Generic[ScreenResultType], Widget):
|
||||
the origin of the specified region.
|
||||
"""
|
||||
return events.MouseMove(
|
||||
event.widget,
|
||||
event.x - region.x,
|
||||
event.y - region.y,
|
||||
event.delta_x,
|
||||
|
||||
@@ -25,6 +25,8 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
|
||||
index: The index in the list that's currently highlighted.
|
||||
"""
|
||||
|
||||
ALLOW_MAXIMIZE = True
|
||||
|
||||
DEFAULT_CSS = """
|
||||
ListView {
|
||||
background: $surface;
|
||||
|
||||
@@ -25,6 +25,8 @@ class RadioSet(VerticalScroll, can_focus=True, can_focus_children=False):
|
||||
turned off.
|
||||
"""
|
||||
|
||||
ALLOW_MAXIMIZE = True
|
||||
|
||||
DEFAULT_CSS = """
|
||||
RadioSet {
|
||||
border: tall $border-blurred;
|
||||
|
||||
@@ -50,6 +50,7 @@ class Switch(Widget, can_focus=True):
|
||||
background: $surface;
|
||||
height: auto;
|
||||
width: auto;
|
||||
|
||||
padding: 0 2;
|
||||
&.-on > .switch--slider {
|
||||
color: $success;
|
||||
|
||||
Reference in New Issue
Block a user