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 {
|
#thing {
|
||||||
width: 20;
|
width: auto;
|
||||||
height: 10;
|
height: 10;
|
||||||
|
|
||||||
background:magenta;
|
background:magenta;
|
||||||
margin: 1;
|
margin: 1;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
|
border: solid white;
|
||||||
|
box-sizing: content-box;
|
||||||
/* border: solid white; */
|
/* border: solid white; */
|
||||||
align-vertical: middle;
|
align-vertical: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#thing2 {
|
#thing2 {
|
||||||
width: 20;
|
width: auto;
|
||||||
height: 10;
|
height: 10;
|
||||||
|
/* border: solid white; */
|
||||||
|
outline: heavy blue;
|
||||||
|
|
||||||
|
padding: 1 2;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
background:green;
|
background:green;
|
||||||
align-vertical: middle;
|
align-vertical: middle;
|
||||||
|
color:white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from rich.text import Text
|
|||||||
|
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
class Thing(Widget):
|
class Thing(Widget):
|
||||||
@@ -15,7 +16,7 @@ class AlignApp(App):
|
|||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self.log("MOUNTED")
|
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):
|
def action_log_tree(self):
|
||||||
self.log(self.screen.tree)
|
self.log(self.screen.tree)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
App > Screen {
|
App > Screen {
|
||||||
layout: dock;
|
layout: dock;
|
||||||
docks: side=left/1;
|
docks: side=left/1;
|
||||||
background: $background;
|
background: $surface;
|
||||||
color: $text-background;
|
color: $text-surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -67,13 +67,12 @@ App > Screen {
|
|||||||
color: $text-background;
|
color: $text-background;
|
||||||
background: $background;
|
background: $background;
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
overflow-y:scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Tweet {
|
Tweet {
|
||||||
height: 22;
|
height: 12;
|
||||||
max-width: 80;
|
max-width: 80;
|
||||||
margin: 1 3;
|
margin: 1 3;
|
||||||
background: $panel;
|
background: $panel;
|
||||||
@@ -86,6 +85,21 @@ Tweet {
|
|||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
overflow-y: scroll;
|
||||||
|
max-width:80;
|
||||||
|
height: 20;
|
||||||
|
align-horizontal: center;
|
||||||
|
layout: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
|
||||||
|
height: 34;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
TweetHeader {
|
TweetHeader {
|
||||||
height:1;
|
height:1;
|
||||||
@@ -170,7 +184,7 @@ Error {
|
|||||||
margin: 1 3;
|
margin: 1 3;
|
||||||
|
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
Warning {
|
Warning {
|
||||||
@@ -196,3 +210,8 @@ Success {
|
|||||||
text-style: bold;
|
text-style: bold;
|
||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.horizontal {
|
||||||
|
layout: horizontal
|
||||||
|
}
|
||||||
@@ -1,9 +1,47 @@
|
|||||||
from rich.align import Align
|
from rich.align import Align
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
|
from rich.syntax import Syntax
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
from textual.widget import Widget
|
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(
|
lorem = Text.from_markup(
|
||||||
@@ -56,9 +94,25 @@ class BasicApp(App):
|
|||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
"""Build layout here."""
|
"""Build layout here."""
|
||||||
self.mount(
|
self.mount(
|
||||||
header=Widget(),
|
header=Static(
|
||||||
|
Align.center(
|
||||||
|
"[b]This is a [u]Textual[/u] app, running in the terminal",
|
||||||
|
vertical="middle",
|
||||||
|
)
|
||||||
|
),
|
||||||
content=Widget(
|
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(),
|
Error(),
|
||||||
Tweet(TweetBody()),
|
Tweet(TweetBody()),
|
||||||
Warning(),
|
Warning(),
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ DEFAULT_COLORS = ColorSystem(
|
|||||||
success="#6d9f71",
|
success="#6d9f71",
|
||||||
accent="#ffa62b",
|
accent="#ffa62b",
|
||||||
system="#5a4599",
|
system="#5a4599",
|
||||||
|
dark_surface="#292929",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ class ScalarProperty:
|
|||||||
value = obj.get_rule(self.name)
|
value = obj.get_rule(self.name)
|
||||||
return value
|
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
|
"""Set the scalar property
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ from __future__ import annotations
|
|||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
import re
|
import re
|
||||||
from typing import Iterable, NamedTuple, TYPE_CHECKING
|
from typing import Callable, Iterable, NamedTuple, TYPE_CHECKING
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
|
||||||
from textual.css.tokenizer import Token
|
|
||||||
|
|
||||||
from .. import log
|
from .. import log
|
||||||
from ..geometry import Offset
|
from ..geometry import Offset
|
||||||
@@ -108,6 +107,10 @@ class Scalar(NamedTuple):
|
|||||||
def symbol(self) -> str:
|
def symbol(self) -> str:
|
||||||
return UNIT_SYMBOL[self.unit]
|
return UNIT_SYMBOL[self.unit]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_auto(self) -> bool:
|
||||||
|
return self.unit == Unit.AUTO
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_number(cls, value: float) -> Scalar:
|
def from_number(cls, value: float) -> Scalar:
|
||||||
return cls(float(value), Unit.CELLS, Unit.WIDTH)
|
return cls(float(value), Unit.CELLS, Unit.WIDTH)
|
||||||
|
|||||||
@@ -362,71 +362,6 @@ class StylesBase(ABC):
|
|||||||
else:
|
else:
|
||||||
return None
|
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:
|
def align_width(self, width: int, parent_width: int) -> int:
|
||||||
"""Align the width dimension.
|
"""Align the width dimension.
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Functions and classes to manage terminal geometry (anything involving coordinate
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from math import sqrt
|
|
||||||
from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar
|
from typing import Any, cast, NamedTuple, Tuple, Union, TypeVar
|
||||||
|
|
||||||
|
|
||||||
@@ -97,7 +96,7 @@ class Offset(NamedTuple):
|
|||||||
"""
|
"""
|
||||||
x1, y1 = self
|
x1, y1 = self
|
||||||
x2, y2 = other
|
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
|
return distance
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class HorizontalLayout(Layout):
|
|||||||
|
|
||||||
for widget in parent.children:
|
for widget in parent.children:
|
||||||
styles = widget.styles
|
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
|
size, parent_size
|
||||||
)
|
)
|
||||||
offset_y = styles.align_height(
|
offset_y = styles.align_height(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class VerticalLayout(Layout):
|
|||||||
|
|
||||||
for widget in parent.children:
|
for widget in parent.children:
|
||||||
styles = widget.styles
|
styles = widget.styles
|
||||||
(content_width, content_height), margin = styles.get_box_model(
|
(content_width, content_height), margin = widget.get_box_model(
|
||||||
size, parent_size
|
size, parent_size
|
||||||
)
|
)
|
||||||
offset_x = styles.align_width(
|
offset_x = styles.align_width(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from typing import (
|
|||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.align import Align
|
from rich.align import Align
|
||||||
from rich.console import Console, RenderableType
|
from rich.console import Console, RenderableType
|
||||||
|
from rich.measure import Measurement
|
||||||
from rich.padding import Padding
|
from rich.padding import Padding
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
from rich.styled import Styled
|
from rich.styled import Styled
|
||||||
@@ -23,12 +24,13 @@ from . import errors, log
|
|||||||
from . import events
|
from . import events
|
||||||
from ._animator import BoundAnimator
|
from ._animator import BoundAnimator
|
||||||
from ._border import Border
|
from ._border import Border
|
||||||
|
from ._box_model import get_box_model
|
||||||
from ._callback import invoke
|
from ._callback import invoke
|
||||||
from .color import Color
|
from .color import Color
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
from ._types import Lines
|
from ._types import Lines
|
||||||
from .dom import DOMNode
|
from .dom import DOMNode
|
||||||
from .geometry import clamp, Offset, Region, Size
|
from .geometry import clamp, Offset, Region, Size, Spacing
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from . import messages
|
from . import messages
|
||||||
from .layout import Layout
|
from .layout import Layout
|
||||||
@@ -94,6 +96,8 @@ class Widget(DOMNode):
|
|||||||
super().__init__(name=name, id=id, classes=classes)
|
super().__init__(name=name, id=id, classes=classes)
|
||||||
self.add_children(*children)
|
self.add_children(*children)
|
||||||
|
|
||||||
|
auto_width = Reactive(True)
|
||||||
|
auto_height = Reactive(True)
|
||||||
has_focus = Reactive(False)
|
has_focus = Reactive(False)
|
||||||
mouse_over = Reactive(False)
|
mouse_over = Reactive(False)
|
||||||
scroll_x = Reactive(0.0, repaint=False)
|
scroll_x = Reactive(0.0, repaint=False)
|
||||||
@@ -103,6 +107,26 @@ class Widget(DOMNode):
|
|||||||
show_vertical_scrollbar = Reactive(False, layout=True)
|
show_vertical_scrollbar = Reactive(False, layout=True)
|
||||||
show_horizontal_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:
|
async def watch_scroll_x(self, new_value: float) -> None:
|
||||||
self.horizontal_scrollbar.position = int(new_value)
|
self.horizontal_scrollbar.position = int(new_value)
|
||||||
|
|
||||||
@@ -651,13 +675,13 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
def on_mouse_scroll_down(self, event) -> None:
|
def on_mouse_scroll_down(self, event) -> None:
|
||||||
if self.is_container:
|
if self.is_container:
|
||||||
if not self.scroll_down(animate=False):
|
self.scroll_down(animate=False)
|
||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
def on_mouse_scroll_up(self, event) -> None:
|
def on_mouse_scroll_up(self, event) -> None:
|
||||||
if self.is_container:
|
if self.is_container:
|
||||||
if not self.scroll_up(animate=False):
|
self.scroll_up(animate=False)
|
||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
def handle_scroll_to(self, message: ScrollTo) -> None:
|
def handle_scroll_to(self, message: ScrollTo) -> None:
|
||||||
if self.is_container:
|
if self.is_container:
|
||||||
|
|||||||
Reference in New Issue
Block a user