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 { #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;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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