mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
box model
This commit is contained in:
@@ -10,23 +10,31 @@ Widget {
|
||||
}
|
||||
|
||||
#thing {
|
||||
width: 20;
|
||||
width: auto;
|
||||
height: 10;
|
||||
|
||||
background:magenta;
|
||||
margin: 1;
|
||||
padding: 1;
|
||||
border: solid white;
|
||||
box-sizing: content-box;
|
||||
/* border: solid white; */
|
||||
align-vertical: middle;
|
||||
}
|
||||
|
||||
|
||||
#thing2 {
|
||||
width: 20;
|
||||
width: auto;
|
||||
height: 10;
|
||||
/* border: solid white; */
|
||||
outline: heavy blue;
|
||||
|
||||
padding: 1 2;
|
||||
box-sizing: content-box;
|
||||
|
||||
background:green;
|
||||
align-vertical: middle;
|
||||
color:white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from rich.text import Text
|
||||
|
||||
from textual.app import App
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class Thing(Widget):
|
||||
@@ -15,7 +16,7 @@ class AlignApp(App):
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.log("MOUNTED")
|
||||
self.mount(thing=Thing(), thing2=Widget(), thing3=Widget())
|
||||
self.mount(thing=Thing(), thing2=Static("0123456789"), thing3=Widget())
|
||||
|
||||
def action_log_tree(self):
|
||||
self.log(self.screen.tree)
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
App > Screen {
|
||||
layout: dock;
|
||||
docks: side=left/1;
|
||||
background: $background;
|
||||
color: $text-background;
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,13 +67,12 @@ App > Screen {
|
||||
color: $text-background;
|
||||
background: $background;
|
||||
layout: vertical;
|
||||
overflow-y:scroll;
|
||||
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
|
||||
Tweet {
|
||||
height: 22;
|
||||
height: 12;
|
||||
max-width: 80;
|
||||
margin: 1 3;
|
||||
background: $panel;
|
||||
@@ -86,6 +85,21 @@ Tweet {
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
overflow-y: scroll;
|
||||
max-width:80;
|
||||
height: 20;
|
||||
align-horizontal: center;
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
.code {
|
||||
|
||||
height: 34;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
TweetHeader {
|
||||
height:1;
|
||||
@@ -196,3 +210,8 @@ Success {
|
||||
text-style: bold;
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
|
||||
.horizontal {
|
||||
layout: horizontal
|
||||
}
|
||||
@@ -1,9 +1,47 @@
|
||||
from rich.align import Align
|
||||
from rich.console import RenderableType
|
||||
from rich.syntax import Syntax
|
||||
from rich.text import Text
|
||||
|
||||
from textual.app import App
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
CODE = '''
|
||||
class Offset(NamedTuple):
|
||||
"""A point defined by x and y coordinates."""
|
||||
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
|
||||
@property
|
||||
def is_origin(self) -> bool:
|
||||
"""Check if the point is at the origin (0, 0)"""
|
||||
return self == (0, 0)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self != (0, 0)
|
||||
|
||||
def __add__(self, other: object) -> Offset:
|
||||
if isinstance(other, tuple):
|
||||
_x, _y = self
|
||||
x, y = other
|
||||
return Offset(_x + x, _y + y)
|
||||
return NotImplemented
|
||||
|
||||
def __sub__(self, other: object) -> Offset:
|
||||
if isinstance(other, tuple):
|
||||
_x, _y = self
|
||||
x, y = other
|
||||
return Offset(_x - x, _y - y)
|
||||
return NotImplemented
|
||||
|
||||
def __mul__(self, other: object) -> Offset:
|
||||
if isinstance(other, (float, int)):
|
||||
x, y = self
|
||||
return Offset(int(x * other), int(y * other))
|
||||
return NotImplemented
|
||||
'''
|
||||
|
||||
|
||||
lorem = Text.from_markup(
|
||||
@@ -56,9 +94,25 @@ class BasicApp(App):
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
self.mount(
|
||||
header=Widget(),
|
||||
header=Static(
|
||||
Align.center(
|
||||
"[b]This is a [u]Textual[/u] app, running in the terminal",
|
||||
vertical="middle",
|
||||
)
|
||||
),
|
||||
content=Widget(
|
||||
Tweet(TweetBody(), Widget(classes={"button"})),
|
||||
Tweet(
|
||||
TweetBody(),
|
||||
# Widget(
|
||||
# Widget(classes={"button"}),
|
||||
# Widget(classes={"button"}),
|
||||
# classes={"horizontal"},
|
||||
# ),
|
||||
),
|
||||
Widget(
|
||||
Static(Syntax(CODE, "python"), classes={"code"}),
|
||||
classes={"scrollable"},
|
||||
),
|
||||
Error(),
|
||||
Tweet(TweetBody()),
|
||||
Warning(),
|
||||
|
||||
@@ -64,6 +64,7 @@ DEFAULT_COLORS = ColorSystem(
|
||||
success="#6d9f71",
|
||||
accent="#ffa62b",
|
||||
system="#5a4599",
|
||||
dark_surface="#292929",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,7 +76,9 @@ class ScalarProperty:
|
||||
value = obj.get_rule(self.name)
|
||||
return value
|
||||
|
||||
def __set__(self, obj: StylesBase, value: float | Scalar | str | None) -> None:
|
||||
def __set__(
|
||||
self, obj: StylesBase, value: float | int | Scalar | str | None
|
||||
) -> None:
|
||||
"""Set the scalar property
|
||||
|
||||
Args:
|
||||
|
||||
@@ -3,11 +3,10 @@ from __future__ import annotations
|
||||
from enum import Enum, unique
|
||||
from functools import lru_cache
|
||||
import re
|
||||
from typing import Iterable, NamedTuple, TYPE_CHECKING
|
||||
from typing import Callable, Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
import rich.repr
|
||||
|
||||
from textual.css.tokenizer import Token
|
||||
|
||||
from .. import log
|
||||
from ..geometry import Offset
|
||||
@@ -108,6 +107,10 @@ class Scalar(NamedTuple):
|
||||
def symbol(self) -> str:
|
||||
return UNIT_SYMBOL[self.unit]
|
||||
|
||||
@property
|
||||
def is_auto(self) -> bool:
|
||||
return self.unit == Unit.AUTO
|
||||
|
||||
@classmethod
|
||||
def from_number(cls, value: float) -> Scalar:
|
||||
return cls(float(value), Unit.CELLS, Unit.WIDTH)
|
||||
|
||||
@@ -362,71 +362,6 @@ class StylesBase(ABC):
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_box_model(
|
||||
self, container_size: Size, parent_size: Size
|
||||
) -> tuple[Size, Spacing]:
|
||||
"""Resolve the box model for this Styles.
|
||||
|
||||
Args:
|
||||
container_size (Size): The size of the widget container.
|
||||
parent_size (Size): The size widget's parent.
|
||||
|
||||
Returns:
|
||||
tuple[Size, Spacing]: A tuple with the size of the content area and margin.
|
||||
"""
|
||||
has_rule = self.has_rule
|
||||
width, height = container_size
|
||||
|
||||
if has_rule("width"):
|
||||
width = self.width.resolve_dimension(container_size, parent_size)
|
||||
else:
|
||||
width = max(0, width - self.margin.width)
|
||||
|
||||
if self.min_width:
|
||||
min_width = self.min_width.resolve_dimension(container_size, parent_size)
|
||||
width = max(width, min_width)
|
||||
|
||||
if self.max_width:
|
||||
max_width = self.max_width.resolve_dimension(container_size, parent_size)
|
||||
width = min(width, max_width)
|
||||
|
||||
if has_rule("height"):
|
||||
height = self.height.resolve_dimension(container_size, parent_size)
|
||||
else:
|
||||
height = max(0, height - self.margin.height)
|
||||
|
||||
if self.min_height:
|
||||
min_height = self.min_height.resolve_dimension(container_size, parent_size)
|
||||
height = max(height, min_height)
|
||||
|
||||
if self.max_height:
|
||||
max_height = self.max_height.resolve_dimension(container_size, parent_size)
|
||||
height = min(width, max_height)
|
||||
|
||||
# TODO: box sizing
|
||||
|
||||
size = Size(width, height)
|
||||
margin = Spacing(0, 0, 0, 0)
|
||||
|
||||
if self.box_sizing == "content-box":
|
||||
|
||||
if has_rule("padding"):
|
||||
size += self.padding.totals
|
||||
if has_rule("border"):
|
||||
size += self.border.spacing.totals
|
||||
if has_rule("margin"):
|
||||
margin = self.margin
|
||||
|
||||
else: # border-box
|
||||
# if has_rule("padding"):
|
||||
# size -= self.padding.totals
|
||||
if has_rule("border"):
|
||||
size -= self.border.spacing.totals
|
||||
if has_rule("margin"):
|
||||
margin = self.margin
|
||||
|
||||
return size, margin
|
||||
|
||||
def align_width(self, width: int, parent_width: int) -> int:
|
||||
"""Align the width dimension.
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ Functions and classes to manage terminal geometry (anything involving coordinate
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from math import sqrt
|
||||
from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar
|
||||
|
||||
|
||||
@@ -97,7 +96,7 @@ class Offset(NamedTuple):
|
||||
"""
|
||||
x1, y1 = self
|
||||
x2, y2 = other
|
||||
distance = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
|
||||
distance = ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) ** 0.5
|
||||
return distance
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class HorizontalLayout(Layout):
|
||||
|
||||
for widget in parent.children:
|
||||
styles = widget.styles
|
||||
(content_width, content_height), margin = widget.styles.get_box_model(
|
||||
(content_width, content_height), margin = widget.get_box_model(
|
||||
size, parent_size
|
||||
)
|
||||
offset_y = styles.align_height(
|
||||
|
||||
@@ -28,7 +28,7 @@ class VerticalLayout(Layout):
|
||||
|
||||
for widget in parent.children:
|
||||
styles = widget.styles
|
||||
(content_width, content_height), margin = styles.get_box_model(
|
||||
(content_width, content_height), margin = widget.get_box_model(
|
||||
size, parent_size
|
||||
)
|
||||
offset_x = styles.align_width(
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import (
|
||||
import rich.repr
|
||||
from rich.align import Align
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.measure import Measurement
|
||||
from rich.padding import Padding
|
||||
from rich.style import Style
|
||||
from rich.styled import Styled
|
||||
@@ -23,12 +24,13 @@ from . import errors, log
|
||||
from . import events
|
||||
from ._animator import BoundAnimator
|
||||
from ._border import Border
|
||||
from ._box_model import get_box_model
|
||||
from ._callback import invoke
|
||||
from .color import Color
|
||||
from ._context import active_app
|
||||
from ._types import Lines
|
||||
from .dom import DOMNode
|
||||
from .geometry import clamp, Offset, Region, Size
|
||||
from .geometry import clamp, Offset, Region, Size, Spacing
|
||||
from .message import Message
|
||||
from . import messages
|
||||
from .layout import Layout
|
||||
@@ -94,6 +96,8 @@ class Widget(DOMNode):
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
self.add_children(*children)
|
||||
|
||||
auto_width = Reactive(True)
|
||||
auto_height = Reactive(True)
|
||||
has_focus = Reactive(False)
|
||||
mouse_over = Reactive(False)
|
||||
scroll_x = Reactive(0.0, repaint=False)
|
||||
@@ -103,6 +107,26 @@ class Widget(DOMNode):
|
||||
show_vertical_scrollbar = Reactive(False, layout=True)
|
||||
show_horizontal_scrollbar = Reactive(False, layout=True)
|
||||
|
||||
def get_box_model(self, container_size, parent_size) -> tuple[Size, Spacing]:
|
||||
box_model = get_box_model(
|
||||
self.styles,
|
||||
container_size,
|
||||
parent_size,
|
||||
self.get_content_width,
|
||||
self.get_content_height,
|
||||
)
|
||||
self.log(self, self.styles.padding, self.styles.border.spacing)
|
||||
return box_model
|
||||
|
||||
def get_content_width(self, container_size: Size, parent_size: Size) -> int:
|
||||
console = self.app.console
|
||||
renderable = self.render()
|
||||
measurement = Measurement.get(console, console.options, renderable)
|
||||
return measurement.maximum
|
||||
|
||||
def get_content_height(self, container_size: Size, parent_size: Size) -> int:
|
||||
return container_size.height
|
||||
|
||||
async def watch_scroll_x(self, new_value: float) -> None:
|
||||
self.horizontal_scrollbar.position = int(new_value)
|
||||
|
||||
@@ -651,13 +675,13 @@ class Widget(DOMNode):
|
||||
|
||||
def on_mouse_scroll_down(self, event) -> None:
|
||||
if self.is_container:
|
||||
if not self.scroll_down(animate=False):
|
||||
event.stop()
|
||||
self.scroll_down(animate=False)
|
||||
event.stop()
|
||||
|
||||
def on_mouse_scroll_up(self, event) -> None:
|
||||
if self.is_container:
|
||||
if not self.scroll_up(animate=False):
|
||||
event.stop()
|
||||
self.scroll_up(animate=False)
|
||||
event.stop()
|
||||
|
||||
def handle_scroll_to(self, message: ScrollTo) -> None:
|
||||
if self.is_container:
|
||||
|
||||
Reference in New Issue
Block a user