mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'css' into inline-styles-view
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
40
src/textual/layouts/horizontal.py
Normal file
40
src/textual/layouts/horizontal.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,6 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from . import log
|
||||
from . import events
|
||||
|
||||
from ._callback import count_parameters, invoke
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user