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.
|
||||
- [max-width](../styles/max_width.md) sets a maximum width.
|
||||
- [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
|
||||
|
||||
|
||||
@@ -16,10 +16,6 @@ CodeBrowser.-show-tree #tree-view {
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
CodeBrowser{
|
||||
background: $background;
|
||||
}
|
||||
|
||||
DirectoryTree {
|
||||
padding-right: 1;
|
||||
padding-right: 1;
|
||||
|
||||
@@ -1213,6 +1213,8 @@ class App(Generic[ReturnType], DOMNode):
|
||||
apply_stylesheet = self.stylesheet.apply
|
||||
|
||||
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_id is not None:
|
||||
widget.id = widget_id
|
||||
|
||||
@@ -18,6 +18,7 @@ class BorderButtons(layout.Vertical):
|
||||
BorderButtons {
|
||||
dock: left;
|
||||
width: 24;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
BorderButtons > Button {
|
||||
|
||||
@@ -3,7 +3,7 @@ EasingButtons > Button {
|
||||
}
|
||||
EasingButtons {
|
||||
dock: left;
|
||||
overflow: auto auto;
|
||||
overflow-y: scroll;
|
||||
width: 20;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from .geometry import Offset, Region, Size, Spacing, clamp
|
||||
from .layouts.vertical import VerticalLayout
|
||||
from .message import Message
|
||||
from .reactive import Reactive
|
||||
from .render import measure
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .app import App, ComposeResult
|
||||
@@ -61,7 +62,9 @@ class RenderCache(NamedTuple):
|
||||
@rich.repr.auto
|
||||
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_children: bool = True
|
||||
fluid = Reactive(True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -317,6 +321,8 @@ class Widget(DOMNode):
|
||||
self.get_content_width,
|
||||
self.get_content_height,
|
||||
)
|
||||
self.log(self)
|
||||
self.log(box_model)
|
||||
return box_model
|
||||
|
||||
def get_content_width(self, container: Size, viewport: Size) -> int:
|
||||
@@ -342,12 +348,10 @@ class Widget(DOMNode):
|
||||
console = self.app.console
|
||||
renderable = self.post_render(self.render())
|
||||
|
||||
measurement = Measurement.get(
|
||||
console,
|
||||
console.options.update_width(container.width),
|
||||
renderable,
|
||||
)
|
||||
width = measurement.maximum
|
||||
width = measure(console, renderable, container.width)
|
||||
if self.fluid:
|
||||
width = min(width, container.width)
|
||||
|
||||
self._content_width_cache = (cache_key, width)
|
||||
return width
|
||||
|
||||
@@ -493,6 +497,8 @@ class Widget(DOMNode):
|
||||
overflow_y = styles.overflow_y
|
||||
width, height = self.container_size
|
||||
|
||||
previous_show_vertical = self.show_vertical_scrollbar
|
||||
|
||||
show_horizontal = self.show_horizontal_scrollbar
|
||||
if overflow_x == "hidden":
|
||||
show_horizontal = False
|
||||
@@ -509,10 +515,15 @@ class Widget(DOMNode):
|
||||
elif overflow_y == "auto":
|
||||
show_vertical = self.virtual_size.height > height
|
||||
|
||||
if show_vertical and not show_horizontal and overflow_x == "auto":
|
||||
show_horizontal = (
|
||||
self.virtual_size.width + styles.scrollbar_size_vertical > width
|
||||
)
|
||||
# if (
|
||||
# not previous_show_vertical
|
||||
# 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_vertical_scrollbar = show_vertical
|
||||
@@ -1168,12 +1179,21 @@ class Widget(DOMNode):
|
||||
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.
|
||||
|
||||
Args:
|
||||
widget (Widget): A descendant widget.
|
||||
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:
|
||||
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:
|
||||
container = widget.parent
|
||||
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:
|
||||
scrolled = True
|
||||
@@ -1206,7 +1230,13 @@ class Widget(DOMNode):
|
||||
return scrolled
|
||||
|
||||
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:
|
||||
"""Scrolls a given region in to view, if required.
|
||||
|
||||
@@ -1216,8 +1246,9 @@ class Widget(DOMNode):
|
||||
Args:
|
||||
region (Region): A region that should be visible.
|
||||
spacing (Spacing | None, optional): Optional spacing around the region. Defaults to None.
|
||||
animate (bool, optional): Enable animation. Defaults to True.
|
||||
spacing (Spacing): Space to subtract from the window region.
|
||||
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:
|
||||
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,
|
||||
)
|
||||
if delta:
|
||||
if speed is None and duration is None:
|
||||
duration = 0.2
|
||||
self.scroll_relative(
|
||||
delta.x or None,
|
||||
delta.y or None,
|
||||
animate=animate if (abs(delta_y) > 1 or delta_x) else False,
|
||||
duration=0.2,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
)
|
||||
return delta
|
||||
|
||||
def scroll_visible(self) -> None:
|
||||
"""Scroll the container to make this widget visible."""
|
||||
def scroll_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
|
||||
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__(
|
||||
cls,
|
||||
@@ -1476,7 +1527,10 @@ class Widget(DOMNode):
|
||||
"""
|
||||
if self._dirty_regions:
|
||||
self._render_content()
|
||||
try:
|
||||
line = self._render_cache.lines[y]
|
||||
except IndexError:
|
||||
line = [Segment(" " * self.size.width, self.rich_style)]
|
||||
return line
|
||||
|
||||
def render_lines(self, crop: Region) -> Lines:
|
||||
@@ -1657,7 +1711,11 @@ class Widget(DOMNode):
|
||||
def _on_mount(self, event: events.Mount) -> None:
|
||||
widgets = self.compose()
|
||||
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:
|
||||
self.mouse_over = False
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
from rich.console import RenderableType
|
||||
from rich.protocol import is_renderable
|
||||
|
||||
from ..reactive import Reactive
|
||||
from ..errors import RenderError
|
||||
from ..widget import Widget
|
||||
|
||||
@@ -20,11 +19,22 @@ def _check_renderable(renderable: object):
|
||||
"""
|
||||
if not is_renderable(renderable):
|
||||
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):
|
||||
"""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 = """
|
||||
Static {
|
||||
height: auto;
|
||||
@@ -35,18 +45,31 @@ class Static(Widget):
|
||||
self,
|
||||
renderable: RenderableType = "",
|
||||
*,
|
||||
fluid: bool = True,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
self.renderable = renderable
|
||||
self._renderable = renderable
|
||||
self.fluid = fluid
|
||||
_check_renderable(renderable)
|
||||
|
||||
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:
|
||||
"""Update the widget contents.
|
||||
|
||||
Args:
|
||||
renderable (RenderableType): A new rich renderable.
|
||||
"""
|
||||
_check_renderable(renderable)
|
||||
self.renderable = renderable
|
||||
self._renderable = renderable
|
||||
self.refresh(layout=True)
|
||||
|
||||
Reference in New Issue
Block a user