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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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