fluid rendering

This commit is contained in:
Will McGugan
2022-09-18 11:25:16 +01:00
parent 614b29e222
commit 9838d3b34e
9 changed files with 114 additions and 71 deletions

View File

@@ -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.

View File

@@ -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"

View File

@@ -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

View File

@@ -16,10 +16,6 @@ CodeBrowser.-show-tree #tree-view {
background: $surface;
}
CodeBrowser{
background: $background;
}
DirectoryTree {
padding-right: 1;
padding-right: 1;

View File

@@ -583,7 +583,7 @@ class App(Generic[ReturnType], DOMNode):
filename (str | None, optional): Filename of SVG screenshot, or None to auto-generate
a filename with the date and time. Defaults to None.
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:
str: Filename of screenshot.
@@ -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

View File

@@ -18,6 +18,7 @@ class BorderButtons(layout.Vertical):
BorderButtons {
dock: left;
width: 24;
overflow-y: scroll;
}
BorderButtons > Button {

View File

@@ -3,7 +3,7 @@ EasingButtons > Button {
}
EasingButtons {
dock: left;
overflow: auto auto;
overflow-y: scroll;
width: 20;
}

View File

@@ -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()
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
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

View File

@@ -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)