mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
28
sandbox/will/center.py
Normal file
28
sandbox/will/center.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from textual.app import App
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class CenterApp(App):
|
||||
CSS = """
|
||||
|
||||
Screen {
|
||||
layout: center;
|
||||
overflow: auto auto;
|
||||
}
|
||||
|
||||
Static {
|
||||
border: wide $primary;
|
||||
background: $panel;
|
||||
width: 50;
|
||||
height: 20;
|
||||
margin: 1 2;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def compose(self):
|
||||
yield Static("Hello World!")
|
||||
|
||||
|
||||
app = CenterApp()
|
||||
55
sandbox/will/center2.py
Normal file
55
sandbox/will/center2.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from textual.app import App
|
||||
from textual.layout import Vertical, Center
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class CenterApp(App):
|
||||
CSS = """
|
||||
|
||||
#sidebar {
|
||||
dock: left;
|
||||
width: 32;
|
||||
height: 100%;
|
||||
border-right: vkey $primary;
|
||||
}
|
||||
|
||||
#bottombar {
|
||||
dock: bottom;
|
||||
height: 12;
|
||||
width: 100%;
|
||||
border-top: hkey $primary;
|
||||
}
|
||||
|
||||
#hello {
|
||||
border: wide $primary;
|
||||
width: 40;
|
||||
height: 16;
|
||||
margin: 2 4;
|
||||
}
|
||||
|
||||
#sidebar.hidden {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
Static {
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.bind("t", "toggle_class('#sidebar', 'hidden')")
|
||||
|
||||
def compose(self):
|
||||
yield Static("Sidebar", id="sidebar")
|
||||
yield Vertical(
|
||||
Static("Bottom bar", id="bottombar"),
|
||||
Center(
|
||||
Static("Hello World!", id="hello"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
app = CenterApp(log_verbosity=3)
|
||||
@@ -680,7 +680,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
finally:
|
||||
self.mouse_over = widget
|
||||
|
||||
async def capture_mouse(self, widget: Widget | None) -> None:
|
||||
def capture_mouse(self, widget: Widget | None) -> None:
|
||||
"""Send all mouse events to the given widget, disable mouse capture.
|
||||
|
||||
Args:
|
||||
@@ -689,12 +689,12 @@ class App(Generic[ReturnType], DOMNode):
|
||||
if widget == self.mouse_captured:
|
||||
return
|
||||
if self.mouse_captured is not None:
|
||||
await self.mouse_captured.post_message(
|
||||
self.mouse_captured.post_message_no_wait(
|
||||
events.MouseRelease(self, self.mouse_position)
|
||||
)
|
||||
self.mouse_captured = widget
|
||||
if widget is not None:
|
||||
await widget.post_message(events.MouseCapture(self, self.mouse_position))
|
||||
widget.post_message_no_wait(events.MouseCapture(self, self.mouse_position))
|
||||
|
||||
def panic(self, *renderables: RenderableType) -> None:
|
||||
"""Exits the app then displays a message.
|
||||
|
||||
@@ -81,7 +81,7 @@ def get_box_model(
|
||||
)
|
||||
content_width = min(content_width, max_width)
|
||||
|
||||
content_width = max(Fraction(1), content_width)
|
||||
content_width = max(Fraction(0), content_width)
|
||||
|
||||
if styles.height is None:
|
||||
# No height specified, fill the available space
|
||||
|
||||
@@ -32,7 +32,7 @@ VALID_BORDER: Final[set[EdgeType]] = {
|
||||
"wide",
|
||||
}
|
||||
VALID_EDGE: Final = {"top", "right", "bottom", "left"}
|
||||
VALID_LAYOUT: Final = {"vertical", "horizontal"}
|
||||
VALID_LAYOUT: Final = {"vertical", "horizontal", "center"}
|
||||
|
||||
VALID_BOX_SIZING: Final = {"border-box", "content-box"}
|
||||
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"}
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
class Vertical(Widget):
|
||||
"""A container widget to align children vertically."""
|
||||
class Container(Widget):
|
||||
"""Simple container widget, with vertical layout."""
|
||||
|
||||
CSS = """
|
||||
Vertical {
|
||||
Container {
|
||||
layout: vertical;
|
||||
overflow: auto;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class Horizontal(Widget):
|
||||
class Vertical(Container):
|
||||
"""A container widget to align children vertically."""
|
||||
|
||||
|
||||
class Horizontal(Container):
|
||||
"""A container widget to align children horizontally."""
|
||||
|
||||
CSS = """
|
||||
Horizontal {
|
||||
layout: horizontal;
|
||||
overflow: auto;
|
||||
layout: horizontal;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class Center(Container):
|
||||
"""A container widget to align children in the center."""
|
||||
|
||||
CSS = """
|
||||
Center {
|
||||
layout: center;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
39
src/textual/layouts/center.py
Normal file
39
src/textual/layouts/center.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fractions import Fraction
|
||||
|
||||
from .._layout import ArrangeResult, Layout, WidgetPlacement
|
||||
from ..geometry import Region, Size
|
||||
from ..widget import Widget
|
||||
|
||||
|
||||
class CenterLayout(Layout):
|
||||
"""Positions widgets in the center of the screen."""
|
||||
|
||||
name = "center"
|
||||
|
||||
def arrange(
|
||||
self, parent: Widget, children: list[Widget], size: Size
|
||||
) -> ArrangeResult:
|
||||
|
||||
placements: list[WidgetPlacement] = []
|
||||
total_regions: list[Region] = []
|
||||
|
||||
parent_size = parent.outer_size
|
||||
container_width, container_height = size
|
||||
fraction_unit = Fraction(size.width)
|
||||
|
||||
for widget in children:
|
||||
width, height, margin = widget.get_box_model(
|
||||
size, parent_size, fraction_unit
|
||||
)
|
||||
margin_width = width + margin.width
|
||||
margin_height = height + margin.height
|
||||
x = margin.left + max(0, (container_width - margin_width) // 2)
|
||||
y = margin.top + max(0, (container_height - margin_height) // 2)
|
||||
region = Region(x, y, int(width), int(height))
|
||||
total_regions.append(region.grow(margin))
|
||||
placements.append(WidgetPlacement(region, widget, 0))
|
||||
|
||||
placements.append(WidgetPlacement(Region.from_union(total_regions), None, 0))
|
||||
return placements, set(children)
|
||||
@@ -1,11 +1,15 @@
|
||||
from .horizontal import HorizontalLayout
|
||||
from __future__ import annotations
|
||||
|
||||
from .._layout import Layout
|
||||
from ..layouts.vertical import VerticalLayout
|
||||
from .horizontal import HorizontalLayout
|
||||
from .vertical import VerticalLayout
|
||||
from .center import CenterLayout
|
||||
|
||||
|
||||
LAYOUT_MAP = {
|
||||
LAYOUT_MAP: dict[str, type[Layout]] = {
|
||||
"vertical": VerticalLayout,
|
||||
"horizontal": HorizontalLayout,
|
||||
"center": CenterLayout,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# TODO: This is deprecated (and probably doesn't work any more)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
@@ -170,7 +170,6 @@ class Screen(Widget):
|
||||
self.update_timer.pause()
|
||||
try:
|
||||
hidden, shown, resized = self._compositor.reflow(self, size)
|
||||
|
||||
Hide = events.Hide
|
||||
Show = events.Show
|
||||
for widget in hidden:
|
||||
|
||||
@@ -231,10 +231,14 @@ class ScrollBar(Widget):
|
||||
style=scrollbar_style,
|
||||
)
|
||||
|
||||
async def on_enter(self, event: events.Enter) -> None:
|
||||
def on_hide(self, event: events.Hide) -> None:
|
||||
if self.grabbed:
|
||||
self.release_mouse()
|
||||
|
||||
def on_enter(self, event: events.Enter) -> None:
|
||||
self.mouse_over = True
|
||||
|
||||
async def on_leave(self, event: events.Leave) -> None:
|
||||
def on_leave(self, event: events.Leave) -> None:
|
||||
self.mouse_over = False
|
||||
|
||||
async def action_scroll_down(self) -> None:
|
||||
@@ -243,21 +247,21 @@ class ScrollBar(Widget):
|
||||
async def action_scroll_up(self) -> None:
|
||||
await self.emit(ScrollUp(self) if self.vertical else ScrollLeft(self))
|
||||
|
||||
async def action_grab(self) -> None:
|
||||
await self.capture_mouse()
|
||||
def action_grab(self) -> None:
|
||||
self.capture_mouse()
|
||||
|
||||
async def action_released(self) -> None:
|
||||
await self.capture_mouse(False)
|
||||
def action_released(self) -> None:
|
||||
self.capture_mouse(False)
|
||||
|
||||
async def on_mouse_up(self, event: events.MouseUp) -> None:
|
||||
if self.grabbed:
|
||||
await self.release_mouse()
|
||||
self.release_mouse()
|
||||
|
||||
async def on_mouse_capture(self, event: events.MouseCapture) -> None:
|
||||
def on_mouse_capture(self, event: events.MouseCapture) -> None:
|
||||
self.grabbed = event.mouse_position
|
||||
self.grabbed_position = self.position
|
||||
|
||||
async def on_mouse_release(self, event: events.MouseRelease) -> None:
|
||||
def on_mouse_release(self, event: events.MouseRelease) -> None:
|
||||
self.grabbed = None
|
||||
|
||||
async def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
|
||||
@@ -1061,7 +1061,9 @@ class Widget(DOMNode):
|
||||
|
||||
if layout:
|
||||
self._layout_required = True
|
||||
self._clear_arrangement_cache()
|
||||
if isinstance(self._parent, Widget):
|
||||
self._parent._clear_arrangement_cache()
|
||||
|
||||
if repaint:
|
||||
self._set_dirty(*regions)
|
||||
self._content_width_cache = (None, 0)
|
||||
@@ -1115,7 +1117,7 @@ class Widget(DOMNode):
|
||||
"""Give input focus to this widget."""
|
||||
self.app.set_focus(self)
|
||||
|
||||
async def capture_mouse(self, capture: bool = True) -> None:
|
||||
def capture_mouse(self, capture: bool = True) -> None:
|
||||
"""Capture (or release) the mouse.
|
||||
|
||||
When captured, all mouse coordinates will go to this widget even when the pointer is not directly over the widget.
|
||||
@@ -1123,14 +1125,14 @@ class Widget(DOMNode):
|
||||
Args:
|
||||
capture (bool, optional): True to capture or False to release. Defaults to True.
|
||||
"""
|
||||
await self.app.capture_mouse(self if capture else None)
|
||||
self.app.capture_mouse(self if capture else None)
|
||||
|
||||
async def release_mouse(self) -> None:
|
||||
def release_mouse(self) -> None:
|
||||
"""Release the mouse.
|
||||
|
||||
Mouse events will only be sent when the mouse is over the widget.
|
||||
"""
|
||||
await self.app.capture_mouse(None)
|
||||
self.app.capture_mouse(None)
|
||||
|
||||
async def broker_event(self, event_name: str, event: events.Event) -> bool:
|
||||
return await self.app.broker_event(event_name, event, default_namespace=self)
|
||||
@@ -1159,10 +1161,10 @@ class Widget(DOMNode):
|
||||
self.mount(*widgets)
|
||||
self.screen.refresh(repaint=False, layout=True)
|
||||
|
||||
def on_leave(self) -> None:
|
||||
def on_leave(self, event: events.Leave) -> None:
|
||||
self.mouse_over = False
|
||||
|
||||
def on_enter(self) -> None:
|
||||
def on_enter(self, event: events.Enter) -> None:
|
||||
self.mouse_over = True
|
||||
|
||||
def on_focus(self, event: events.Focus) -> None:
|
||||
|
||||
33
tests/test_layouts_center.py
Normal file
33
tests/test_layouts_center.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from textual._layout import WidgetPlacement
|
||||
from textual.geometry import Region, Size
|
||||
from textual.layouts.center import CenterLayout
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
def test_center_layout():
|
||||
|
||||
widget = Widget()
|
||||
widget._size = Size(80, 24)
|
||||
child = Widget()
|
||||
child.styles.width = 10
|
||||
child.styles.height = 5
|
||||
layout = CenterLayout()
|
||||
|
||||
placements, widgets = layout.arrange(widget, [child], Size(60, 20))
|
||||
assert widgets == {child}
|
||||
|
||||
expected = [
|
||||
WidgetPlacement(
|
||||
region=Region(x=25, y=7, width=10, height=5),
|
||||
widget=child,
|
||||
order=0,
|
||||
fixed=False,
|
||||
),
|
||||
WidgetPlacement(
|
||||
region=Region(x=25, y=7, width=10, height=5),
|
||||
widget=None,
|
||||
order=0,
|
||||
fixed=False,
|
||||
),
|
||||
]
|
||||
assert placements == expected
|
||||
Reference in New Issue
Block a user