renamed Region.origin to offset

This commit is contained in:
Will McGugan
2022-06-22 11:47:58 +01:00
parent 3451152993
commit e85438c9e6
9 changed files with 468 additions and 40 deletions

View File

@@ -112,7 +112,6 @@ class BasicApp(App, css_path="basic.css"):
Tweet(TweetBody()),
Widget(
Static(Syntax(CODE, "python"), classes="code"),
self.scroll_to_target,
classes="scrollable",
),
Error(),

231
sandbox/will/basic.css Normal file
View File

@@ -0,0 +1,231 @@
/* CSS file for basic.py */
* {
transition: color 300ms linear, background 300ms linear;
}
* {
scrollbar-background: $panel-darken-2;
scrollbar-background-hover: $panel-darken-3;
scrollbar-color: $system;
scrollbar-color-active: $accent-darken-1;
scrollbar-size-horizontal: 1;
scrollbar-size-vertical: 2;
}
App > Screen {
layout: dock;
docks: side=left/1;
background: $surface;
color: $text-surface;
}
#sidebar {
color: $text-primary;
background: $primary-background;
dock: side;
width: 30;
offset-x: -100%;
layout: dock;
transition: offset 500ms in_out_cubic;
}
#sidebar.-active {
offset-x: 0;
}
#sidebar .title {
height: 3;
background: $primary-background-darken-2;
color: $text-primary-darken-2 ;
border-right: outer $primary-darken-3;
content-align: center middle;
}
#sidebar .user {
height: 8;
background: $primary-background-darken-1;
color: $text-primary-darken-1;
border-right: outer $primary-background-darken-3;
content-align: center middle;
}
#sidebar .content {
background: $primary-background;
color: $text-primary-background;
border-right: outer $primary-background-darken-3;
content-align: center middle;
}
#header {
color: $text-primary-darken-1;
background: $primary-darken-1;
height: 3;
content-align: center middle;
}
#content {
color: $text-background;
background: $background;
layout: vertical;
overflow-y: scroll;
}
Tweet {
height:12;
width: 100%;
margin: 1 3;
background: $panel;
color: $text-panel;
layout: vertical;
/* border: outer $primary; */
padding: 1;
border: wide $panel-darken-2;
overflow: auto;
/* scrollbar-gutter: stable; */
align-horizontal: center;
box-sizing: border-box;
}
.scrollable {
overflow-y: scroll;
margin: 1 2;
height: 20;
align-horizontal: center;
layout: vertical;
}
.code {
height: auto;
}
TweetHeader {
height:1;
background: $accent;
color: $text-accent
}
TweetBody {
width: 100%;
background: $panel;
color: $text-panel;
height: auto;
padding: 0 1 0 0;
}
Tweet.scroll-horizontal TweetBody {
width: 350;
}
.button {
background: $accent;
color: $text-accent;
width:20;
height: 3;
/* border-top: hidden $accent-darken-3; */
border: tall $accent-darken-2;
/* border-left: tall $accent-darken-1; */
/* padding: 1 0 0 0 ; */
transition: background 400ms in_out_cubic, color 400ms in_out_cubic;
}
.button:hover {
background: $accent-lighten-1;
color: $text-accent-lighten-1;
width: 20;
height: 3;
border: tall $accent-darken-1;
/* border-left: tall $accent-darken-3; */
}
#footer {
color: $text-accent;
background: $accent;
height: 1;
border-top: hkey $accent-darken-2;
content-align: center middle;
}
#sidebar .content {
layout: vertical
}
OptionItem {
height: 3;
background: $primary-background;
border-right: outer $primary-background-darken-2;
border-left: blank;
content-align: center middle;
}
OptionItem:hover {
height: 3;
color: $accent;
background: $primary-background-darken-1;
/* border-top: hkey $accent2-darken-3;
border-bottom: hkey $accent2-darken-3; */
text-style: bold;
border-left: outer $accent-darken-2;
}
Error {
width: 100%;
height:3;
background: $error;
color: $text-error;
border-top: hkey $error-darken-2;
border-bottom: hkey $error-darken-2;
margin: 1 3;
text-style: bold;
align-horizontal: center;
}
Warning {
width: 100%;
height:3;
background: $warning;
color: $text-warning-fade-1;
border-top: hkey $warning-darken-2;
border-bottom: hkey $warning-darken-2;
margin: 1 2;
text-style: bold;
align-horizontal: center;
}
Success {
width: 100%;
height:3;
box-sizing: border-box;
background: $success-lighten-3;
color: $text-success-lighten-3-fade-1;
border-top: hkey $success;
border-bottom: hkey $success;
margin: 1 2;
text-style: bold;
align-horizontal: center;
}
.horizontal {
layout: horizontal
}

185
sandbox/will/basic.py Normal file
View File

@@ -0,0 +1,185 @@
from rich.console import RenderableType
from rich.style import Style
from rich.syntax import Syntax
from rich.text import Text
from textual.app import App
from textual.reactive import Reactive
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_short = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit liber a a a, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum."""
lorem = (
lorem_short
+ """ In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """
)
lorem_short_text = Text.from_markup(lorem_short)
lorem_long_text = Text.from_markup(lorem * 2)
class TweetHeader(Widget):
def render(self) -> RenderableType:
return Text("Lorem Impsum", justify="center")
class TweetBody(Widget):
short_lorem = Reactive(False)
def render(self) -> Text:
return lorem_short_text if self.short_lorem else lorem_long_text
class Tweet(Widget):
pass
class OptionItem(Widget):
def render(self) -> Text:
return Text("Option")
class Error(Widget):
def render(self) -> Text:
return Text("This is an error message", justify="center")
class Warning(Widget):
def render(self) -> Text:
return Text("This is a warning message", justify="center")
class Success(Widget):
def render(self) -> Text:
return Text("This is a success message", justify="center")
class BasicApp(App, css_path="basic.css"):
"""A basic app demonstrating CSS"""
def on_load(self):
"""Bind keys here."""
self.bind("s", "toggle_class('#sidebar', '-active')")
def on_mount(self):
"""Build layout here."""
self.scroll_to_target = Tweet(TweetBody())
self.mount(
header=Static(
Text.from_markup(
"[b]This is a [u]Textual[/u] app, running in the terminal"
),
),
content=Widget(
Tweet(TweetBody()),
Widget(
Static(Syntax(CODE, "python"), classes="code"),
classes="scrollable",
),
Error(),
Tweet(TweetBody(), classes="scrollbar-size-custom"),
Warning(),
Tweet(TweetBody(), classes="scroll-horizontal"),
Success(),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
),
footer=Widget(),
sidebar=Widget(
Widget(classes="title"),
Widget(classes="user"),
OptionItem(),
OptionItem(),
OptionItem(),
Widget(classes="content"),
),
)
async def on_key(self, event) -> None:
await self.dispatch_key(event)
def key_d(self):
self.dark = not self.dark
async def key_q(self):
await self.shutdown()
def key_x(self):
self.panic(self.tree)
def key_escape(self):
self.app.bell()
def key_t(self):
# Pressing "t" toggles the content of the TweetBody widget, from a long "Lorem ipsum..." to a shorter one.
tweet_body = self.query("TweetBody").first()
tweet_body.short_lorem = not tweet_body.short_lorem
def key_v(self):
self.get_child(id="content").scroll_to_widget(self.scroll_to_target)
def key_space(self):
self.bell()
app = BasicApp()
if __name__ == "__main__":
app.run()
from textual.geometry import Region
from textual.color import Color
print(Region.intersection.cache_info())
print(Region.overlaps.cache_info())
print(Region.union.cache_info())
print(Region.split_vertical.cache_info())
print(Region.__contains__.cache_info())
from textual.css.scalar import Scalar
print(Scalar.resolve_dimension.cache_info())
from rich.style import Style
print(Style._add.cache_info())

View File

@@ -328,7 +328,7 @@ class Compositor:
sub_clip = clip.intersection(child_region)
# The region covered by children relative to parent widget
total_region = child_region.reset_origin
total_region = child_region.reset_offset
if widget.is_container:
# Arrange the layout
@@ -338,7 +338,7 @@ class Compositor:
# An offset added to all placements
placement_offset = (
container_region.origin + layout_offset - widget.scroll_offset
container_region.offset + layout_offset - widget.scroll_offset
)
# Add all the widgets
@@ -358,7 +358,7 @@ class Compositor:
container_size
):
map[chrome_widget] = MapGeometry(
chrome_region + container_region.origin + layout_offset,
chrome_region + container_region.offset + layout_offset,
order,
clip,
container_size,
@@ -414,7 +414,7 @@ class Compositor:
def get_offset(self, widget: Widget) -> Offset:
"""Get the offset of a widget."""
try:
return self.map[widget].region.origin
return self.map[widget].region.offset
except KeyError:
raise errors.NoWidget("Widget is not in layout")

View File

@@ -224,17 +224,17 @@ class Region(NamedTuple):
return cls(x1, y1, x2 - x1, y2 - y1)
@classmethod
def from_origin(cls, origin: tuple[int, int], size: tuple[int, int]) -> Region:
"""Create a region from origin and size.
def from_offset(cls, offset: tuple[int, int], size: tuple[int, int]) -> Region:
"""Create a region from offset and size.
Args:
origin (Point): Origin (top left point)
offset (Point): Offset (top left point)
size (tuple[int, int]): Dimensions of region.
Returns:
Region: A region instance.
"""
x, y = origin
x, y = offset
width, height = size
return cls(x, y, width, height)
@@ -280,7 +280,7 @@ class Region(NamedTuple):
return self.width * self.height
@property
def origin(self) -> Offset:
def offset(self) -> Offset:
"""Get the start point of the region."""
return Offset(self.x, self.y)
@@ -328,7 +328,7 @@ class Region(NamedTuple):
return range(self.y, self.y + self.height)
@property
def reset_origin(self) -> Region:
def reset_offset(self) -> Region:
"""An region of the same size at (0, 0)."""
_, _, width, height = self
return Region(0, 0, width, height)
@@ -347,6 +347,19 @@ class Region(NamedTuple):
return Region(x - ox, y - oy, width, height)
return NotImplemented
def at_offset(self, offset: tuple[int, int]) -> Region:
"""Get a new Region with the same size at a given offset.
Args:
offset (tuple[int, int]): An offset.
Returns:
Region: New Region with adjusted offset.
"""
x, y = offset
_x, _y, width, height = self
return Region(x, y, width, height)
def expand(self, size: tuple[int, int]) -> Region:
"""Increase the size of the region by adding a border.
@@ -430,7 +443,7 @@ class Region(NamedTuple):
)
def translate(self, x: int = 0, y: int = 0) -> Region:
"""Move the origin of the Region.
"""Move the offset of the Region.
Args:
translate_x (int): Value to add to x coordinate.

View File

@@ -595,9 +595,9 @@ class Widget(DOMNode):
# We can either scroll so the widget is at the top of the container, or so that
# it is at the bottom. We want to pick which has the shortest distance
top_delta = widget_region.origin - container_region.origin
top_delta = widget_region.offset - container_region.origin
bottom_delta = widget_region.origin - (
bottom_delta = widget_region.offset - (
container_region.origin
+ Offset(0, container_region.height - widget_region.height)
)
@@ -627,7 +627,10 @@ class Widget(DOMNode):
def scroll_to_region(
self, region: Region, *, spacing: Spacing | None = None, animate: bool = True
) -> bool:
"""Scrolls a given region in to view.
"""Scrolls a given region in to view, if required.
This method will scroll the least distance required to move `region` fully within
the scrollable area.
Args:
region (Region): A region that should be visible.
@@ -638,9 +641,7 @@ class Widget(DOMNode):
bool: True if the window was scrolled.
"""
scroll_x, scroll_y = self.scroll_offset
width, height = self.region.size
window = Region(scroll_x, scroll_y, width, height)
window = self.region.at_offset(self.scroll_offset)
if spacing is not None:
window = window.shrink(spacing)
@@ -650,13 +651,13 @@ class Widget(DOMNode):
window_left, window_top, window_right, window_bottom = window.corners
left, top, right, bottom = region.corners
delta_x = delta_y = 0
if not (
(window_right > left >= window_left)
and (window_right > right >= window_left)
):
# The window needs to scroll on the X axis to bring region in to view
delta_x = min(
left - window_left,
left - (window_right - region.width),
@@ -667,6 +668,7 @@ class Widget(DOMNode):
(window_bottom > top >= window_top)
and (window_bottom > bottom >= window_top)
):
# The window needs to scroll on the Y axis to bring region in to view
delta_y = min(
top - window_top,
top - (window_bottom - region.height),

View File

@@ -130,13 +130,15 @@ class DataTable(ScrollView, Generic[CellType]):
self._y_offsets: list[tuple[int, int]] = []
self._row_render_cache: LRUCache[tuple[int, int, Style], tuple[Lines, Lines]]
self._row_render_cache: LRUCache[
tuple[int, int, Style, int, int], tuple[Lines, Lines]
]
self._row_render_cache = LRUCache(1000)
self._cell_render_cache: LRUCache[tuple[int, int, Style], Lines]
self._cell_render_cache: LRUCache[tuple[int, int, Style, bool, bool], Lines]
self._cell_render_cache = LRUCache(1000)
self._line_cache: LRUCache[tuple[int, int, int, int], list[Segment]]
self._line_cache: LRUCache[tuple[int, int, int, int, int, int], list[Segment]]
self._line_cache = LRUCache(1000)
self._line_no = 0
@@ -148,10 +150,10 @@ class DataTable(ScrollView, Generic[CellType]):
header_height = Reactive(1)
show_cursor = Reactive(True)
cursor_type = Reactive(CELL)
cursor_row = Reactive(0)
cursor_column = Reactive(1)
hover_row = Reactive(0)
hover_column = Reactive(0)
cursor_row: Reactive[int] = Reactive(0)
cursor_column: Reactive[int] = Reactive(1)
hover_row: Reactive[int] = Reactive(0)
hover_column: Reactive[int] = Reactive(0)
def _clear_caches(self) -> None:
self._row_render_cache.clear()
@@ -408,13 +410,7 @@ class DataTable(ScrollView, Generic[CellType]):
scrollable_line: list[Segment] = list(chain.from_iterable(scrollable))
segments = fixed_line + line_crop(scrollable_line, x1 + fixed_width, x2, width)
# remaining_width = width - (fixed_width + min(width, (x2 - x1 + fixed_width)))
# if remaining_width > 0:
# segments.append(Segment(" " * remaining_width, base_style))
# elif remaining_width < 0:
segments = Segment.adjust_line_length(segments, width, style=base_style)
simplified_segments = list(Segment.simplify(segments))
self._line_cache[cache_key] = simplified_segments
@@ -456,8 +452,9 @@ class DataTable(ScrollView, Generic[CellType]):
def on_mouse_move(self, event: events.MouseMove):
meta = self.get_style_at(event.x, event.y).meta
self.hover_row = meta.get("row")
self.hover_column = meta.get("column")
if meta:
self.hover_row = meta["row"]
self.hover_column = meta["column"]
async def on_key(self, event) -> None:
await self.dispatch_key(event)
@@ -479,9 +476,10 @@ class DataTable(ScrollView, Generic[CellType]):
def on_click(self, event: events.Click) -> None:
meta = self.get_style_at(event.x, event.y).meta
self.cursor_row = meta.get("row")
self.cursor_column = meta.get("column")
self._scroll_cursor_in_to_view()
if meta:
self.cursor_row = meta["row"]
self.cursor_column = meta["column"]
self._scroll_cursor_in_to_view()
def key_down(self, event: events.Key):
self.cursor_row += 1

View File

@@ -126,7 +126,7 @@ def test_region_from_union():
def test_region_from_origin():
assert Region.from_origin(Offset(3, 4), (5, 6)) == Region(3, 4, 5, 6)
assert Region.from_offset(Offset(3, 4), (5, 6)) == Region(3, 4, 5, 6)
def test_region_area():
@@ -140,7 +140,7 @@ def test_region_size():
def test_region_origin():
assert Region(1, 2, 3, 4).origin == Offset(1, 2)
assert Region(1, 2, 3, 4).offset == Offset(1, 2)
def test_region_bottom_left():
@@ -275,7 +275,7 @@ def test_region_y_range():
def test_region_reset_origin():
assert Region(5, 10, 20, 30).reset_origin == Region(0, 0, 20, 30)
assert Region(5, 10, 20, 30).reset_offset == Region(0, 0, 20, 30)
def test_region_expand():