mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fluid rendering
This commit is contained in:
@@ -1,18 +0,0 @@
|
|||||||
# Documentation Workflow
|
|
||||||
|
|
||||||
* Ensure you're inside a *Python 3.10+* virtual environment
|
|
||||||
* Run the live-reload server using `mkdocs serve` from the project root
|
|
||||||
* Create new pages by adding new directories and Markdown files inside `docs/*`
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
- `mkdocs serve` - Start the live-reloading docs server.
|
|
||||||
- `mkdocs build` - Build the documentation site.
|
|
||||||
- `mkdocs -h` - Print help message and exit.
|
|
||||||
|
|
||||||
## Project layout
|
|
||||||
|
|
||||||
mkdocs.yml # The configuration file.
|
|
||||||
docs/
|
|
||||||
index.md # The documentation homepage.
|
|
||||||
... # Other markdown pages, images and other files.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from textual.app import App
|
|
||||||
from textual.widgets import Button
|
|
||||||
|
|
||||||
|
|
||||||
class ButtonApp(App):
|
|
||||||
|
|
||||||
DEFAULT_CSS = """
|
|
||||||
Button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
yield Button("Lights off")
|
|
||||||
|
|
||||||
def on_button_pressed(self, event):
|
|
||||||
self.dark = not self.dark
|
|
||||||
self.bell()
|
|
||||||
event.button.label = "Lights ON" if self.dark else "Lights OFF"
|
|
||||||
@@ -206,7 +206,7 @@ The same units may also be used to set limits on a dimension. The following styl
|
|||||||
- [min-width](../styles/min_width.md) sets a minimum width.
|
- [min-width](../styles/min_width.md) sets a minimum width.
|
||||||
- [max-width](../styles/max_width.md) sets a maximum width.
|
- [max-width](../styles/max_width.md) sets a maximum width.
|
||||||
- [min-height](../styles/min_height.md) sets a minimum height.
|
- [min-height](../styles/min_height.md) sets a minimum height.
|
||||||
- [max-height](../styles/max_hright.md) sets a maximum height.
|
- [max-height](../styles/max_height.md) sets a maximum height.
|
||||||
|
|
||||||
### Padding
|
### Padding
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ CodeBrowser.-show-tree #tree-view {
|
|||||||
background: $surface;
|
background: $surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
CodeBrowser{
|
|
||||||
background: $background;
|
|
||||||
}
|
|
||||||
|
|
||||||
DirectoryTree {
|
DirectoryTree {
|
||||||
padding-right: 1;
|
padding-right: 1;
|
||||||
padding-right: 1;
|
padding-right: 1;
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
filename (str | None, optional): Filename of SVG screenshot, or None to auto-generate
|
filename (str | None, optional): Filename of SVG screenshot, or None to auto-generate
|
||||||
a filename with the date and time. Defaults to None.
|
a filename with the date and time. Defaults to None.
|
||||||
path (str, optional): Path to directory for output. Defaults to current working directory.
|
path (str, optional): Path to directory for output. Defaults to current working directory.
|
||||||
time_format(str, optional): Time format to use if filename is None. Defaults to "%Y-%m-%d %X %f".
|
time_format (str, optional): Time format to use if filename is None. Defaults to "%Y-%m-%d %X %f".
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Filename of screenshot.
|
str: Filename of screenshot.
|
||||||
@@ -1213,6 +1213,8 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
apply_stylesheet = self.stylesheet.apply
|
apply_stylesheet = self.stylesheet.apply
|
||||||
|
|
||||||
for widget_id, widget in name_widgets:
|
for widget_id, widget in name_widgets:
|
||||||
|
if not isinstance(widget, Widget):
|
||||||
|
raise AppError(f"Can't register {widget!r}; expected a Widget instance")
|
||||||
if widget not in self._registry:
|
if widget not in self._registry:
|
||||||
if widget_id is not None:
|
if widget_id is not None:
|
||||||
widget.id = widget_id
|
widget.id = widget_id
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class BorderButtons(layout.Vertical):
|
|||||||
BorderButtons {
|
BorderButtons {
|
||||||
dock: left;
|
dock: left;
|
||||||
width: 24;
|
width: 24;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderButtons > Button {
|
BorderButtons > Button {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ EasingButtons > Button {
|
|||||||
}
|
}
|
||||||
EasingButtons {
|
EasingButtons {
|
||||||
dock: left;
|
dock: left;
|
||||||
overflow: auto auto;
|
overflow-y: scroll;
|
||||||
width: 20;
|
width: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from .geometry import Offset, Region, Size, Spacing, clamp
|
|||||||
from .layouts.vertical import VerticalLayout
|
from .layouts.vertical import VerticalLayout
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive
|
||||||
|
from .render import measure
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .app import App, ComposeResult
|
from .app import App, ComposeResult
|
||||||
@@ -61,7 +62,9 @@ class RenderCache(NamedTuple):
|
|||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Widget(DOMNode):
|
class Widget(DOMNode):
|
||||||
"""
|
"""
|
||||||
A Widget is the base class for Textual widgets. Extent this class (or a sub-class) when defining your own widgets.
|
A Widget is the base class for Textual widgets.
|
||||||
|
|
||||||
|
See also [static][textual.widgets._static.Static] for starting point for your own widgets.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -80,6 +83,7 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
can_focus: bool = False
|
can_focus: bool = False
|
||||||
can_focus_children: bool = True
|
can_focus_children: bool = True
|
||||||
|
fluid = Reactive(True)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -317,6 +321,8 @@ class Widget(DOMNode):
|
|||||||
self.get_content_width,
|
self.get_content_width,
|
||||||
self.get_content_height,
|
self.get_content_height,
|
||||||
)
|
)
|
||||||
|
self.log(self)
|
||||||
|
self.log(box_model)
|
||||||
return box_model
|
return box_model
|
||||||
|
|
||||||
def get_content_width(self, container: Size, viewport: Size) -> int:
|
def get_content_width(self, container: Size, viewport: Size) -> int:
|
||||||
@@ -342,12 +348,10 @@ class Widget(DOMNode):
|
|||||||
console = self.app.console
|
console = self.app.console
|
||||||
renderable = self.post_render(self.render())
|
renderable = self.post_render(self.render())
|
||||||
|
|
||||||
measurement = Measurement.get(
|
width = measure(console, renderable, container.width)
|
||||||
console,
|
if self.fluid:
|
||||||
console.options.update_width(container.width),
|
width = min(width, container.width)
|
||||||
renderable,
|
|
||||||
)
|
|
||||||
width = measurement.maximum
|
|
||||||
self._content_width_cache = (cache_key, width)
|
self._content_width_cache = (cache_key, width)
|
||||||
return width
|
return width
|
||||||
|
|
||||||
@@ -493,6 +497,8 @@ class Widget(DOMNode):
|
|||||||
overflow_y = styles.overflow_y
|
overflow_y = styles.overflow_y
|
||||||
width, height = self.container_size
|
width, height = self.container_size
|
||||||
|
|
||||||
|
previous_show_vertical = self.show_vertical_scrollbar
|
||||||
|
|
||||||
show_horizontal = self.show_horizontal_scrollbar
|
show_horizontal = self.show_horizontal_scrollbar
|
||||||
if overflow_x == "hidden":
|
if overflow_x == "hidden":
|
||||||
show_horizontal = False
|
show_horizontal = False
|
||||||
@@ -509,10 +515,15 @@ class Widget(DOMNode):
|
|||||||
elif overflow_y == "auto":
|
elif overflow_y == "auto":
|
||||||
show_vertical = self.virtual_size.height > height
|
show_vertical = self.virtual_size.height > height
|
||||||
|
|
||||||
if show_vertical and not show_horizontal and overflow_x == "auto":
|
# if (
|
||||||
show_horizontal = (
|
# not previous_show_vertical
|
||||||
self.virtual_size.width + styles.scrollbar_size_vertical > width
|
# and show_vertical
|
||||||
)
|
# and show_horizontal
|
||||||
|
# and overflow_x == "auto"
|
||||||
|
# ):
|
||||||
|
# show_horizontal = (
|
||||||
|
# self.virtual_size.width - styles.scrollbar_size_vertical > width
|
||||||
|
# )
|
||||||
|
|
||||||
self.show_horizontal_scrollbar = show_horizontal
|
self.show_horizontal_scrollbar = show_horizontal
|
||||||
self.show_vertical_scrollbar = show_vertical
|
self.show_vertical_scrollbar = show_vertical
|
||||||
@@ -1168,12 +1179,21 @@ class Widget(DOMNode):
|
|||||||
duration=duration,
|
duration=duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_to_widget(self, widget: Widget, *, animate: bool = True) -> bool:
|
def scroll_to_widget(
|
||||||
|
self,
|
||||||
|
widget: Widget,
|
||||||
|
*,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Scroll scrolling to bring a widget in to view.
|
"""Scroll scrolling to bring a widget in to view.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
widget (Widget): A descendant widget.
|
widget (Widget): A descendant widget.
|
||||||
animate (bool, optional): True to animate, or False to jump. Defaults to True.
|
animate (bool, optional): True to animate, or False to jump. Defaults to True.
|
||||||
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any scrolling has occurred in any descendant, otherwise False.
|
bool: True if any scrolling has occurred in any descendant, otherwise False.
|
||||||
@@ -1186,7 +1206,11 @@ class Widget(DOMNode):
|
|||||||
while isinstance(widget.parent, Widget) and widget is not self:
|
while isinstance(widget.parent, Widget) and widget is not self:
|
||||||
container = widget.parent
|
container = widget.parent
|
||||||
scroll_offset = container.scroll_to_region(
|
scroll_offset = container.scroll_to_region(
|
||||||
region, spacing=widget.parent.gutter, animate=animate
|
region,
|
||||||
|
spacing=widget.parent.gutter,
|
||||||
|
animate=animate,
|
||||||
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
)
|
)
|
||||||
if scroll_offset:
|
if scroll_offset:
|
||||||
scrolled = True
|
scrolled = True
|
||||||
@@ -1206,7 +1230,13 @@ class Widget(DOMNode):
|
|||||||
return scrolled
|
return scrolled
|
||||||
|
|
||||||
def scroll_to_region(
|
def scroll_to_region(
|
||||||
self, region: Region, *, spacing: Spacing | None = None, animate: bool = True
|
self,
|
||||||
|
region: Region,
|
||||||
|
*,
|
||||||
|
spacing: Spacing | None = None,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
) -> Offset:
|
) -> Offset:
|
||||||
"""Scrolls a given region in to view, if required.
|
"""Scrolls a given region in to view, if required.
|
||||||
|
|
||||||
@@ -1216,8 +1246,9 @@ class Widget(DOMNode):
|
|||||||
Args:
|
Args:
|
||||||
region (Region): A region that should be visible.
|
region (Region): A region that should be visible.
|
||||||
spacing (Spacing | None, optional): Optional spacing around the region. Defaults to None.
|
spacing (Spacing | None, optional): Optional spacing around the region. Defaults to None.
|
||||||
animate (bool, optional): Enable animation. Defaults to True.
|
animate (bool, optional): True to animate, or False to jump. Defaults to True.
|
||||||
spacing (Spacing): Space to subtract from the window region.
|
speed (float | None, optional): Speed of scroll if animate is True. Or None to use duration.
|
||||||
|
duration (float | None, optional): Duration of animation, if animate is True and speed is None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Offset: The distance that was scrolled.
|
Offset: The distance that was scrolled.
|
||||||
@@ -1236,19 +1267,39 @@ class Widget(DOMNode):
|
|||||||
clamp(scroll_y + delta_y, 0, self.max_scroll_y) - scroll_y,
|
clamp(scroll_y + delta_y, 0, self.max_scroll_y) - scroll_y,
|
||||||
)
|
)
|
||||||
if delta:
|
if delta:
|
||||||
|
if speed is None and duration is None:
|
||||||
|
duration = 0.2
|
||||||
self.scroll_relative(
|
self.scroll_relative(
|
||||||
delta.x or None,
|
delta.x or None,
|
||||||
delta.y or None,
|
delta.y or None,
|
||||||
animate=animate if (abs(delta_y) > 1 or delta_x) else False,
|
animate=animate if (abs(delta_y) > 1 or delta_x) else False,
|
||||||
duration=0.2,
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
)
|
)
|
||||||
return delta
|
return delta
|
||||||
|
|
||||||
def scroll_visible(self) -> None:
|
def scroll_visible(
|
||||||
"""Scroll the container to make this widget visible."""
|
self,
|
||||||
|
animate: bool = True,
|
||||||
|
speed: float | None = None,
|
||||||
|
duration: float | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Scroll the container to make this widget visible.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
animate (bool, optional): _description_. Defaults to True.
|
||||||
|
speed (float | None, optional): _description_. Defaults to None.
|
||||||
|
duration (float | None, optional): _description_. Defaults to None.
|
||||||
|
"""
|
||||||
parent = self.parent
|
parent = self.parent
|
||||||
if isinstance(parent, Widget):
|
if isinstance(parent, Widget):
|
||||||
self.call_later(parent.scroll_to_widget, self)
|
self.call_later(
|
||||||
|
parent.scroll_to_widget,
|
||||||
|
self,
|
||||||
|
animate=animate,
|
||||||
|
speed=speed,
|
||||||
|
duration=duration,
|
||||||
|
)
|
||||||
|
|
||||||
def __init_subclass__(
|
def __init_subclass__(
|
||||||
cls,
|
cls,
|
||||||
@@ -1476,7 +1527,10 @@ class Widget(DOMNode):
|
|||||||
"""
|
"""
|
||||||
if self._dirty_regions:
|
if self._dirty_regions:
|
||||||
self._render_content()
|
self._render_content()
|
||||||
line = self._render_cache.lines[y]
|
try:
|
||||||
|
line = self._render_cache.lines[y]
|
||||||
|
except IndexError:
|
||||||
|
line = [Segment(" " * self.size.width, self.rich_style)]
|
||||||
return line
|
return line
|
||||||
|
|
||||||
def render_lines(self, crop: Region) -> Lines:
|
def render_lines(self, crop: Region) -> Lines:
|
||||||
@@ -1657,7 +1711,11 @@ class Widget(DOMNode):
|
|||||||
def _on_mount(self, event: events.Mount) -> None:
|
def _on_mount(self, event: events.Mount) -> None:
|
||||||
widgets = self.compose()
|
widgets = self.compose()
|
||||||
self.mount(*widgets)
|
self.mount(*widgets)
|
||||||
self.screen.refresh(repaint=False, layout=True)
|
# Preset scrollbars if not automatic
|
||||||
|
if self.styles.overflow_y == "scroll":
|
||||||
|
self.show_vertical_scrollbar = True
|
||||||
|
if self.styles.overflow_x == "scroll":
|
||||||
|
self.show_horizontal_scrollbar = True
|
||||||
|
|
||||||
def _on_leave(self, event: events.Leave) -> None:
|
def _on_leave(self, event: events.Leave) -> None:
|
||||||
self.mouse_over = False
|
self.mouse_over = False
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
from rich.protocol import is_renderable
|
from rich.protocol import is_renderable
|
||||||
|
|
||||||
from ..reactive import Reactive
|
|
||||||
from ..errors import RenderError
|
from ..errors import RenderError
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
@@ -20,11 +19,22 @@ def _check_renderable(renderable: object):
|
|||||||
"""
|
"""
|
||||||
if not is_renderable(renderable):
|
if not is_renderable(renderable):
|
||||||
raise RenderError(
|
raise RenderError(
|
||||||
f"unable to render {renderable!r}; A string, Text, or other Rich renderable is required"
|
f"unable to render {renderable!r}; a string, Text, or other Rich renderable is required"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Static(Widget):
|
class Static(Widget):
|
||||||
|
"""A widget to display simple static content, or use as a base- lass for more complex widgets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
renderable (RenderableType, optional): A Rich renderable, or string containing console markup.
|
||||||
|
Defaults to "".
|
||||||
|
fluid (bool, optional): Enable fluid content (adapts to size of window). Defaults to True.
|
||||||
|
name (str | None, optional): Name of widget. Defaults to None.
|
||||||
|
id (str | None, optional): ID of Widget. Defaults to None.
|
||||||
|
classes (str | None, optional): Space separated list of class names. Defaults to None.
|
||||||
|
"""
|
||||||
|
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
Static {
|
Static {
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -35,18 +45,31 @@ class Static(Widget):
|
|||||||
self,
|
self,
|
||||||
renderable: RenderableType = "",
|
renderable: RenderableType = "",
|
||||||
*,
|
*,
|
||||||
|
fluid: bool = True,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
classes: str | None = None,
|
classes: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__(name=name, id=id, classes=classes)
|
super().__init__(name=name, id=id, classes=classes)
|
||||||
self.renderable = renderable
|
self._renderable = renderable
|
||||||
|
self.fluid = fluid
|
||||||
_check_renderable(renderable)
|
_check_renderable(renderable)
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return self.renderable
|
"""Get a rich renderable for the widget's content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RenderableType: A rich renderable.
|
||||||
|
"""
|
||||||
|
return self._renderable
|
||||||
|
|
||||||
def update(self, renderable: RenderableType) -> None:
|
def update(self, renderable: RenderableType) -> None:
|
||||||
|
"""Update the widget contents.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
renderable (RenderableType): A new rich renderable.
|
||||||
|
"""
|
||||||
_check_renderable(renderable)
|
_check_renderable(renderable)
|
||||||
self.renderable = renderable
|
self._renderable = renderable
|
||||||
self.refresh(layout=True)
|
self.refresh(layout=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user