Merge branch 'css' into inline-styles-view

This commit is contained in:
Will McGugan
2022-02-07 11:44:45 +00:00
committed by GitHub
11 changed files with 96 additions and 147 deletions

View File

@@ -1,4 +1,4 @@
/* CSS file for dev_sandbox.py */
/* CSS file for basic.py */
App > View {
docks: side=left/1;
@@ -36,11 +36,6 @@ Widget:hover {
#content {
text: white on #20639b;
border-bottom: hkey #0f2b41;
offset-y: -3;
}
#content.-content-visible {
visibility: hidden;
}
#footer {

View File

@@ -1,32 +1,23 @@
from rich.console import RenderableType
from rich.panel import Panel
from textual.app import App
from textual.widget import Widget
class PanelWidget(Widget):
def render(self) -> RenderableType:
return Panel("hello world!", title="Title")
class BasicApp(App):
"""Sandbox application used for testing/development by Textual developers"""
"""A basic app demonstrating CSS"""
def on_load(self):
"""Bind keys here."""
self.bind("tab", "toggle_class('#sidebar', '-active')")
self.bind("a", "toggle_class('#header', '-visible')")
self.bind("c", "toggle_class('#content', '-content-visible')")
def on_mount(self):
"""Build layout here."""
self.mount(
header=Widget(),
content=PanelWidget(),
content=Widget(),
footer=Widget(),
sidebar=Widget(),
)
BasicApp.run(css_file="test_app.css", watch_css=True, log="textual.log")
BasicApp.run(css_file="dev_sandbox.css", watch_css=True, log="textual.log")

View File

@@ -309,7 +309,6 @@ class App(DOMNode):
Args:
widget (Widget): [description]
"""
log("set_focus", widget)
if widget == self.focused:
# Widget is already focused
return
@@ -442,7 +441,7 @@ class App(DOMNode):
if self.log_file is not None:
self.log_file.close()
def _register(self, parent: DOMNode, child: DOMNode) -> bool:
def _register_child(self, parent: DOMNode, child: DOMNode) -> bool:
if child not in self.registry:
parent.children._append(child)
self.registry.add(child)
@@ -471,7 +470,7 @@ class App(DOMNode):
if widget not in self.registry:
if widget_id is not None:
widget.id = widget_id
self._register(parent, widget)
self._register_child(parent, child=widget)
apply_stylesheet(widget)
for _widget_id, widget in name_widgets:
@@ -494,7 +493,7 @@ class App(DOMNode):
driver.disable_input()
await self.close_messages()
def refresh(self, repaint: bool = True, layout: bool = False) -> None:
def refresh(self) -> None:
sync_available = (
os.environ.get("TERM_PROGRAM", "") != "Apple_Terminal" and not WINDOWS
)
@@ -695,51 +694,3 @@ class App(DOMNode):
self.reset_styles()
self.stylesheet.update(self)
self.view.refresh(layout=True)
if __name__ == "__main__":
import asyncio
from .widgets import Header
from .widgets import Footer
from .widgets import Placeholder
# from .widgets.scroll_view import ScrollView
import os
class MyApp(App):
"""Just a test app."""
async def on_load(self, event: events.Load) -> None:
await self.bind("ctrl+c", "quit", show=False)
await self.bind("q", "quit", "Quit")
await self.bind("x", "bang", "Test error handling")
await self.bind("b", "toggle_sidebar", "Toggle sidebar")
show_bar: Reactive[bool] = Reactive(False)
async def watch_show_bar(self, show_bar: bool) -> None:
self.animator.animate(self.bar, "layout_offset_x", 0 if show_bar else -40)
async def action_toggle_sidebar(self) -> None:
self.show_bar = not self.show_bar
async def on_mount(self, event: events.Mount) -> None:
view = await self.push_view(DockView())
header = Header()
footer = Footer()
self.bar = Placeholder(name="left")
await view.dock(header, edge="top")
await view.dock(footer, edge="bottom")
await view.dock(self.bar, edge="left", size=40, z=1)
self.bar.layout_offset_x = -40
sub_view = DockView()
await sub_view.dock(Placeholder(), Placeholder(), edge="top")
await view.dock(sub_view, edge="left")
MyApp.run(log="textual.log")

View File

@@ -1,28 +1,21 @@
from __future__ import annotations
from abc import ABC, abstractmethod, abstractmethod
from dataclasses import dataclass
import sys
from abc import ABC, abstractmethod
from itertools import chain
from operator import itemgetter
import sys
from typing import ClassVar, Iterable, Iterator, NamedTuple, TYPE_CHECKING
import rich.repr
from rich.console import Console, ConsoleOptions, RenderResult
from rich.control import Control
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
from rich.segment import Segment, SegmentLines
from rich.style import Style
from . import log, panic
from ._loop import loop_last
from .layout_map import LayoutMap
from ._profile import timer
from ._lines import crop_lines
from ._types import Lines
from .geometry import clamp, Region, Offset, Size
from .geometry import Region, Offset, Size
from .layout_map import LayoutMap
PY38 = sys.version_info >= (3, 8)
@@ -56,6 +49,14 @@ class WidgetPlacement(NamedTuple):
order: int = 0
def apply_margin(self) -> "WidgetPlacement":
"""Apply any margin present in the styles of the widget by shrinking the
region appropriately.
Returns:
WidgetPlacement: Returns ``self`` if no ``margin`` styles are present in
the widget. Otherwise, returns a copy of self with a region shrunk to
account for margin.
"""
region, widget, order = self
if widget is not None:
styles = widget.styles
@@ -171,9 +172,9 @@ class Layout(ABC):
"""Generate a layout map that defines where on the screen the widgets will be drawn.
Args:
console (Console): Console instance.
size (Dimensions): Size of container.
viewport (Region): Screen relative viewport.
view (View): The View instance.
size (Size): Size of container.
scroll (Offset): Offset to apply to the Widget placements.
Returns:
Iterable[WidgetPlacement]: An iterable of widget location

View File

@@ -1,5 +1,6 @@
import sys
from .horizontal import HorizontalLayout
from ..layout import Layout
from ..layouts.dock import DockLayout
from ..layouts.grid import GridLayout
@@ -10,7 +11,14 @@ if sys.version_info >= (3, 8):
else:
from typing_extensions import Literal
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
LayoutName = Literal["dock", "grid", "vertical", "horizontal"]
LAYOUT_MAP = {
"dock": DockLayout,
"grid": GridLayout,
"vertical": VerticalLayout,
"horizontal": HorizontalLayout,
}
class MissingLayout(Exception):

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
from typing import Iterable
from textual._loop import loop_last
from textual.css.styles import Styles
from textual.geometry import Size, Offset, Region
from textual.layout import Layout, WidgetPlacement
from textual.view import View
from textual.widget import Widget
class HorizontalLayout(Layout):
"""Used to layout Widgets horizontally on screen, from left to right. Since Widgets naturally
fill the space of their parent container, all widgets used in a horizontal layout should have a specified.
"""
def get_widgets(self, view: View) -> Iterable[Widget]:
return view.children
def arrange(
self, view: View, size: Size, scroll: Offset
) -> Iterable[WidgetPlacement]:
parent_width, parent_height = size
x, y = 0, 0
for last, widget in loop_last(view.children):
styles: Styles = widget.styles
if styles.height:
render_height = int(
styles.height.resolve_dimension(size, view.app.size)
)
else:
render_height = parent_height
if styles.width:
render_width = int(styles.width.resolve_dimension(size, view.app.size))
else:
render_width = parent_width
region = Region(x, y, render_width, render_height)
yield WidgetPlacement(region, widget, order=0)
x += render_width

View File

@@ -2,9 +2,9 @@ from __future__ import annotations
from typing import Iterable, TYPE_CHECKING
from ..geometry import Offset, Region, Size, Spacing, SpacingDimensions
from ..css.styles import Styles
from ..geometry import Offset, Region, Size
from ..layout import Layout, WidgetPlacement
from .._loop import loop_last
if TYPE_CHECKING:
from ..widget import Widget
@@ -12,60 +12,31 @@ if TYPE_CHECKING:
class VerticalLayout(Layout):
name = "vertical"
def __init__(
self,
*,
auto_width: bool = False,
z: int = 0,
gutter: SpacingDimensions = (0, 0, 0, 0),
):
self.auto_width = auto_width
self.z = z
self.gutter = Spacing.unpack(gutter)
self._widgets: list[Widget] = []
self._max_widget_width = 0
super().__init__()
def add(self, widget: Widget) -> None:
self._widgets.append(widget)
self._max_widget_width = max(widget.app.measure(widget), self._max_widget_width)
def clear(self) -> None:
del self._widgets[:]
self._max_widget_width = 0
def get_widgets(self) -> Iterable[Widget]:
return self._widgets
def get_widgets(self, view: View) -> Iterable[Widget]:
return view.children
def arrange(
self, view: View, size: Size, scroll: Offset
) -> Iterable[WidgetPlacement]:
index = 0
width, _height = size
gutter = self.gutter
x, y = self.gutter.top_left
render_width = (
max(width, self._max_widget_width)
if self.auto_width
else width - gutter.width
)
parent_width, parent_height = size
x, y = 0, 0
total_width = render_width
for widget in view.children:
styles: Styles = widget.styles
gutter_height = max(gutter.top, gutter.bottom)
if styles.height:
render_height = int(
styles.height.resolve_dimension(size, view.app.size)
)
else:
render_height = size.height
if styles.width:
render_width = int(styles.width.resolve_dimension(size, view.app.size))
else:
render_width = parent_width
for last, widget in loop_last(self._widgets):
if (
not widget.render_cache
or widget.render_cache.size.width != render_width
):
widget.render_lines_free(render_width)
assert widget.render_cache is not None
render_height = widget.render_cache.size.height
region = Region(x, y, render_width, render_height)
yield WidgetPlacement(region, widget, (self.z, index))
y += render_height + (gutter.bottom if last else gutter_height)
yield WidgetPlacement(Region(0, 0, total_width + gutter.width, y))
yield WidgetPlacement(region, widget, 0)
y += render_height

View File

@@ -13,7 +13,6 @@ from typing import (
TYPE_CHECKING,
)
from . import log
from . import events
from ._callback import count_parameters, invoke

View File

@@ -22,7 +22,6 @@ class View(Widget):
def __init__(self, name: str | None = None, id: str | None = None) -> None:
self.mouse_over: Widget | None = None
self.widgets: set[Widget] = set()
self._mouse_style: Style = Style()
self._mouse_widget: Widget | None = None

View File

@@ -20,7 +20,7 @@ from rich.style import Style
from rich.styled import Styled
from rich.text import Text
from . import errors
from . import errors, log
from . import events
from ._animator import BoundAnimator
from ._border import Border
@@ -37,8 +37,6 @@ from .reactive import watch
if TYPE_CHECKING:
from .view import View
log = getLogger("rich")
class RenderCache(NamedTuple):
size: Size
@@ -231,12 +229,6 @@ class Widget(DOMNode):
lines = self.console.render_lines(renderable, options)
self.render_cache = RenderCache(self.size, lines)
def render_lines_free(self, width: int) -> None:
renderable = self.render_styled()
options = self.console.options.update(width=width, height=None)
lines = self.console.render_lines(renderable, options)
self.render_cache = RenderCache(Size(width, len(lines)), lines)
def _get_lines(self) -> Lines:
"""Get segment lines to render the widget."""
if self.render_cache is None:

View File

@@ -2,6 +2,7 @@ import pytest
from textual.layouts.dock import DockLayout
from textual.layouts.grid import GridLayout
from textual.layouts.horizontal import HorizontalLayout
from textual.layouts.vertical import VerticalLayout
from textual.view import View
@@ -10,6 +11,7 @@ from textual.view import View
["dock", DockLayout],
["grid", GridLayout],
["vertical", VerticalLayout],
["horizontal", HorizontalLayout],
])
def test_view_layout_get_and_set(layout_name, layout_type):
view = View()