box model

This commit is contained in:
Will McGugan
2022-04-20 14:20:44 +01:00
parent cecbf655ab
commit 66ec130726
12 changed files with 135 additions and 89 deletions

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -17,8 +17,8 @@
App > Screen {
layout: dock;
docks: side=left/1;
background: $background;
color: $text-background;
background: $surface;
color: $text-surface;
}
@@ -68,12 +68,11 @@ App > Screen {
background: $background;
layout: vertical;
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
}

View File

@@ -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(),

View File

@@ -64,6 +64,7 @@ DEFAULT_COLORS = ColorSystem(
success="#6d9f71",
accent="#ffa62b",
system="#5a4599",
dark_surface="#292929",
)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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(

View File

@@ -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(

View File

@@ -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,12 +675,12 @@ class Widget(DOMNode):
def on_mouse_scroll_down(self, event) -> None:
if self.is_container:
if not self.scroll_down(animate=False):
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):
self.scroll_up(animate=False)
event.stop()
def handle_scroll_to(self, message: ScrollTo) -> None: