mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Scroll view now uses grid
This commit is contained in:
@@ -74,7 +74,7 @@ class CalculatorApp(App):
|
|||||||
|
|
||||||
layout.place(
|
layout.place(
|
||||||
*buttons.values(),
|
*buttons.values(),
|
||||||
numbers=Static(Padding(numbers, (0, 1)), style="white on rgb(51,51,51)"),
|
numbers=Static(Padding(numbers, (0, 1), style="white on rgb(51,51,51)")),
|
||||||
zero=make_button("0"),
|
zero=make_button("0"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ log = logging.getLogger("rich")
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .widget import Widget, WidgetID
|
from .widget import Widget, WidgetID
|
||||||
|
from .view import View
|
||||||
|
|
||||||
|
|
||||||
class NoWidget(Exception):
|
class NoWidget(Exception):
|
||||||
@@ -137,6 +138,9 @@ class Layout(ABC):
|
|||||||
) -> dict[Widget, OrderedRegion]:
|
) -> dict[Widget, OrderedRegion]:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
async def mount_all(self, view: "View") -> None:
|
||||||
|
await view.mount(*self.get_widgets())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def map(self) -> dict[Widget, OrderedRegion]:
|
def map(self) -> dict[Widget, OrderedRegion]:
|
||||||
return self._layout_map
|
return self._layout_map
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from .._layout_resolve import layout_resolve
|
|||||||
from .._loop import loop_last
|
from .._loop import loop_last
|
||||||
from ..geometry import Dimensions, Point, Region
|
from ..geometry import Dimensions, Point, Region
|
||||||
from ..layout import Layout, OrderedRegion
|
from ..layout import Layout, OrderedRegion
|
||||||
|
from ..view import View
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
@@ -49,14 +50,16 @@ class GridLayout(Layout):
|
|||||||
self.rows: list[GridOptions] = []
|
self.rows: list[GridOptions] = []
|
||||||
self.areas: dict[str, GridArea] = {}
|
self.areas: dict[str, GridArea] = {}
|
||||||
self.widgets: dict[Widget, str | None] = {}
|
self.widgets: dict[Widget, str | None] = {}
|
||||||
self.column_gap = 1
|
self.column_gap = 0
|
||||||
self.row_gap = 1
|
self.row_gap = 0
|
||||||
self.column_repeat = False
|
self.column_repeat = False
|
||||||
self.row_repeat = False
|
self.row_repeat = False
|
||||||
self.column_align: GridAlign = "start"
|
self.column_align: GridAlign = "start"
|
||||||
self.row_align: GridAlign = "start"
|
self.row_align: GridAlign = "start"
|
||||||
self.column_gutter: int = 0
|
self.column_gutter: int = 0
|
||||||
self.row_gutter: int = 0
|
self.row_gutter: int = 0
|
||||||
|
self.hidden_columns: set[str] = set()
|
||||||
|
self.hidden_rows: set[str] = set()
|
||||||
|
|
||||||
if gap is not None:
|
if gap is not None:
|
||||||
if isinstance(gap, tuple):
|
if isinstance(gap, tuple):
|
||||||
@@ -75,6 +78,18 @@ class GridLayout(Layout):
|
|||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
def hide_row(self, row_name: str) -> None:
|
||||||
|
self.hidden_rows.add(row_name)
|
||||||
|
|
||||||
|
def show_row(self, row_name: str) -> None:
|
||||||
|
self.hidden_rows.discard(row_name)
|
||||||
|
|
||||||
|
def hide_column(self, column_name: str) -> None:
|
||||||
|
self.hidden_rows.add(column_name)
|
||||||
|
|
||||||
|
def show_column(self, column_name: str) -> None:
|
||||||
|
self.hidden_rows.discard(column_name)
|
||||||
|
|
||||||
def add_column(
|
def add_column(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@@ -277,14 +292,33 @@ class GridLayout(Layout):
|
|||||||
|
|
||||||
return names, tracks, len(spans), max_size
|
return names, tracks, len(spans), max_size
|
||||||
|
|
||||||
|
def add_widget(widget: Widget, region: Region, order: tuple[int, int]):
|
||||||
|
region = region + offset + widget.layout_offset
|
||||||
|
map[widget] = OrderedRegion(region, order)
|
||||||
|
if isinstance(widget, View):
|
||||||
|
sub_map = widget.layout.generate_map(
|
||||||
|
region.width, region.height, offset=region.origin
|
||||||
|
)
|
||||||
|
map.update(sub_map)
|
||||||
|
|
||||||
container = Dimensions(
|
container = Dimensions(
|
||||||
width - self.column_gutter * 2, height - self.row_gutter * 2
|
width - self.column_gutter * 2, height - self.row_gutter * 2
|
||||||
)
|
)
|
||||||
column_names, column_tracks, column_count, column_size = resolve_tracks(
|
column_names, column_tracks, column_count, column_size = resolve_tracks(
|
||||||
self.columns, container.width, self.column_gap, self.column_repeat
|
[
|
||||||
|
options
|
||||||
|
for options in self.columns
|
||||||
|
if options.name not in self.hidden_columns
|
||||||
|
],
|
||||||
|
container.width,
|
||||||
|
self.column_gap,
|
||||||
|
self.column_repeat,
|
||||||
)
|
)
|
||||||
row_names, row_tracks, row_count, row_size = resolve_tracks(
|
row_names, row_tracks, row_count, row_size = resolve_tracks(
|
||||||
self.rows, container.height, self.row_gap, self.row_repeat
|
[options for options in self.rows if options.name not in self.hidden_rows],
|
||||||
|
container.height,
|
||||||
|
self.row_gap,
|
||||||
|
self.row_repeat,
|
||||||
)
|
)
|
||||||
grid_size = Dimensions(column_size, row_size)
|
grid_size = Dimensions(column_size, row_size)
|
||||||
|
|
||||||
@@ -323,7 +357,8 @@ class GridLayout(Layout):
|
|||||||
self.column_align,
|
self.column_align,
|
||||||
self.row_align,
|
self.row_align,
|
||||||
)
|
)
|
||||||
map[widget] = OrderedRegion(region + gutter, (0, order))
|
# map[widget] = OrderedRegion(region + gutter + offset, (0, order))
|
||||||
|
add_widget(widget, region + gutter, (0, order))
|
||||||
order += 1
|
order += 1
|
||||||
|
|
||||||
# Widgets with no area assigned.
|
# Widgets with no area assigned.
|
||||||
@@ -355,7 +390,8 @@ class GridLayout(Layout):
|
|||||||
self.column_align,
|
self.column_align,
|
||||||
self.row_align,
|
self.row_align,
|
||||||
)
|
)
|
||||||
map[widget] = OrderedRegion(region + gutter, (0, order))
|
# map[widget] = OrderedRegion(region + gutter + offset, (0, order))
|
||||||
|
add_widget(widget, region + gutter, (0, order))
|
||||||
order += 1
|
order += 1
|
||||||
|
|
||||||
return map
|
return map
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
||||||
from rich.padding import Padding, PaddingDimensions
|
from rich.padding import Padding, PaddingDimensions
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
@@ -10,6 +12,8 @@ from .geometry import Dimensions, Point
|
|||||||
from .message import Message
|
from .message import Message
|
||||||
from .widget import Widget, Reactive
|
from .widget import Widget, Reactive
|
||||||
|
|
||||||
|
log = getLogger("rich")
|
||||||
|
|
||||||
|
|
||||||
class PageUpdate(Message):
|
class PageUpdate(Message):
|
||||||
def can_batch(self, message: "Message") -> bool:
|
def can_batch(self, message: "Message") -> bool:
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ class View(Widget):
|
|||||||
region = self.get_widget_region(widget)
|
region = self.get_widget_region(widget)
|
||||||
else:
|
else:
|
||||||
widget, region = self.get_widget_at(event.x, event.y)
|
widget, region = self.get_widget_at(event.x, event.y)
|
||||||
|
log.debug("WIDGET %r", widget)
|
||||||
except NoWidget:
|
except NoWidget:
|
||||||
await self.app.set_mouse_over(None)
|
await self.app.set_mouse_over(None)
|
||||||
else:
|
else:
|
||||||
|
|||||||
58
src/textual/widgets/_button.py
Normal file
58
src/textual/widgets/_button.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from rich.align import Align
|
||||||
|
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||||
|
from rich.padding import Padding
|
||||||
|
from rich.panel import Panel
|
||||||
|
import rich.repr
|
||||||
|
from rich.style import StyleType
|
||||||
|
|
||||||
|
from ..reactive import Reactive
|
||||||
|
from ..widget import Widget
|
||||||
|
|
||||||
|
|
||||||
|
class Expand:
|
||||||
|
def __init__(self, renderable: RenderableType) -> None:
|
||||||
|
self.renderable = renderable
|
||||||
|
|
||||||
|
def __rich_console__(
|
||||||
|
self, console: Console, options: ConsoleOptions
|
||||||
|
) -> RenderResult:
|
||||||
|
width = options.max_width
|
||||||
|
height = options.height or 1
|
||||||
|
yield from console.render(
|
||||||
|
self.renderable, options.update_dimensions(width, height)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonRenderable:
|
||||||
|
def __init__(self, label: RenderableType, style: StyleType = "") -> None:
|
||||||
|
self.label = label
|
||||||
|
self.style = style
|
||||||
|
|
||||||
|
def __rich_console__(
|
||||||
|
self, console: Console, options: ConsoleOptions
|
||||||
|
) -> RenderResult:
|
||||||
|
width = options.max_width
|
||||||
|
height = options.height or 1
|
||||||
|
|
||||||
|
yield Align.center(
|
||||||
|
self.label, vertical="middle", style=self.style, width=width, height=height
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Button(Widget):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
label: RenderableType,
|
||||||
|
name: str | None = None,
|
||||||
|
style: StyleType = "white on dark_blue",
|
||||||
|
):
|
||||||
|
self.label = label
|
||||||
|
self.name = name or str(label)
|
||||||
|
self.style = style
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def render(self) -> RenderableType:
|
||||||
|
return ButtonRenderable(self.label, style=self.style)
|
||||||
|
return Align.center(self.label, vertical="middle", style=self.style)
|
||||||
@@ -7,18 +7,19 @@ from rich.style import StyleType
|
|||||||
|
|
||||||
|
|
||||||
from .. import events
|
from .. import events
|
||||||
|
from ..layouts.grid import GridLayout
|
||||||
from ..message import Message
|
from ..message import Message
|
||||||
from ..scrollbar import ScrollTo, ScrollBar
|
from ..scrollbar import ScrollTo, ScrollBar
|
||||||
from ..geometry import clamp
|
from ..geometry import clamp
|
||||||
from ..page import Page
|
from ..page import Page
|
||||||
from ..views import DockView
|
from ..view import View
|
||||||
from ..reactive import Reactive
|
from ..reactive import Reactive
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger("rich")
|
log = logging.getLogger("rich")
|
||||||
|
|
||||||
|
|
||||||
class ScrollView(DockView):
|
class ScrollView(View):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
renderable: RenderableType | None = None,
|
renderable: RenderableType | None = None,
|
||||||
@@ -29,12 +30,23 @@ class ScrollView(DockView):
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.fluid = fluid
|
self.fluid = fluid
|
||||||
self._vertical_scrollbar = ScrollBar(vertical=True)
|
self._vertical_scrollbar = ScrollBar(vertical=True)
|
||||||
|
self._horizontal_scrollbar = ScrollBar(vertical=False)
|
||||||
self._page = Page(renderable or "", style=style)
|
self._page = Page(renderable or "", style=style)
|
||||||
super().__init__(name=name)
|
layout = GridLayout()
|
||||||
|
layout.add_column("main")
|
||||||
|
layout.add_column("vertical", size=1)
|
||||||
|
layout.add_row("main")
|
||||||
|
layout.add_row("horizontal", size=1)
|
||||||
|
layout.add_areas(
|
||||||
|
content="main,main", vertical="vertical,main", horizontal="main,horizontal"
|
||||||
|
)
|
||||||
|
layout.hide_row("horizontal")
|
||||||
|
super().__init__(name=name, layout=layout)
|
||||||
|
|
||||||
x: Reactive[float] = Reactive(0)
|
x: Reactive[float] = Reactive(0)
|
||||||
y: Reactive[float] = Reactive(0)
|
y: Reactive[float] = Reactive(0)
|
||||||
|
|
||||||
|
target_x: Reactive[float] = Reactive(0)
|
||||||
target_y: Reactive[float] = Reactive(0)
|
target_y: Reactive[float] = Reactive(0)
|
||||||
|
|
||||||
def validate_y(self, value: float) -> float:
|
def validate_y(self, value: float) -> float:
|
||||||
@@ -52,8 +64,15 @@ class ScrollView(DockView):
|
|||||||
self.require_repaint()
|
self.require_repaint()
|
||||||
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
await self.dock(self._vertical_scrollbar, edge="right", size=1)
|
assert isinstance(self.layout, GridLayout)
|
||||||
await self.dock(self._page, edge="top")
|
self.layout.place(
|
||||||
|
content=self._page,
|
||||||
|
vertical=self._vertical_scrollbar,
|
||||||
|
horizontal=self._horizontal_scrollbar,
|
||||||
|
)
|
||||||
|
await self.layout.mount_all(self)
|
||||||
|
# await self.dock(self._vertical_scrollbar, edge="right", size=1)
|
||||||
|
# await self.dock(self._page, edge="top")
|
||||||
|
|
||||||
def scroll_up(self) -> None:
|
def scroll_up(self) -> None:
|
||||||
self.target_y += 1.5
|
self.target_y += 1.5
|
||||||
|
|||||||
Reference in New Issue
Block a user