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()), Tweet(TweetBody()),
Widget( Widget(
Static(Syntax(CODE, "python"), classes="code"), Static(Syntax(CODE, "python"), classes="code"),
self.scroll_to_target,
classes="scrollable", classes="scrollable",
), ),
Error(), 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) sub_clip = clip.intersection(child_region)
# The region covered by children relative to parent widget # 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: if widget.is_container:
# Arrange the layout # Arrange the layout
@@ -338,7 +338,7 @@ class Compositor:
# An offset added to all placements # An offset added to all placements
placement_offset = ( placement_offset = (
container_region.origin + layout_offset - widget.scroll_offset container_region.offset + layout_offset - widget.scroll_offset
) )
# Add all the widgets # Add all the widgets
@@ -358,7 +358,7 @@ class Compositor:
container_size container_size
): ):
map[chrome_widget] = MapGeometry( map[chrome_widget] = MapGeometry(
chrome_region + container_region.origin + layout_offset, chrome_region + container_region.offset + layout_offset,
order, order,
clip, clip,
container_size, container_size,
@@ -414,7 +414,7 @@ class Compositor:
def get_offset(self, widget: Widget) -> Offset: def get_offset(self, widget: Widget) -> Offset:
"""Get the offset of a widget.""" """Get the offset of a widget."""
try: try:
return self.map[widget].region.origin return self.map[widget].region.offset
except KeyError: except KeyError:
raise errors.NoWidget("Widget is not in layout") 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) return cls(x1, y1, x2 - x1, y2 - y1)
@classmethod @classmethod
def from_origin(cls, origin: tuple[int, int], size: tuple[int, int]) -> Region: def from_offset(cls, offset: tuple[int, int], size: tuple[int, int]) -> Region:
"""Create a region from origin and size. """Create a region from offset and size.
Args: Args:
origin (Point): Origin (top left point) offset (Point): Offset (top left point)
size (tuple[int, int]): Dimensions of region. size (tuple[int, int]): Dimensions of region.
Returns: Returns:
Region: A region instance. Region: A region instance.
""" """
x, y = origin x, y = offset
width, height = size width, height = size
return cls(x, y, width, height) return cls(x, y, width, height)
@@ -280,7 +280,7 @@ class Region(NamedTuple):
return self.width * self.height return self.width * self.height
@property @property
def origin(self) -> Offset: def offset(self) -> Offset:
"""Get the start point of the region.""" """Get the start point of the region."""
return Offset(self.x, self.y) return Offset(self.x, self.y)
@@ -328,7 +328,7 @@ class Region(NamedTuple):
return range(self.y, self.y + self.height) return range(self.y, self.y + self.height)
@property @property
def reset_origin(self) -> Region: def reset_offset(self) -> Region:
"""An region of the same size at (0, 0).""" """An region of the same size at (0, 0)."""
_, _, width, height = self _, _, width, height = self
return Region(0, 0, width, height) return Region(0, 0, width, height)
@@ -347,6 +347,19 @@ class Region(NamedTuple):
return Region(x - ox, y - oy, width, height) return Region(x - ox, y - oy, width, height)
return NotImplemented 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: def expand(self, size: tuple[int, int]) -> Region:
"""Increase the size of the region by adding a border. """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: def translate(self, x: int = 0, y: int = 0) -> Region:
"""Move the origin of the Region. """Move the offset of the Region.
Args: Args:
translate_x (int): Value to add to x coordinate. 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 # 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 # 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 container_region.origin
+ Offset(0, container_region.height - widget_region.height) + Offset(0, container_region.height - widget_region.height)
) )
@@ -627,7 +627,10 @@ class Widget(DOMNode):
def scroll_to_region( def scroll_to_region(
self, region: Region, *, spacing: Spacing | None = None, animate: bool = True self, region: Region, *, spacing: Spacing | None = None, animate: bool = True
) -> bool: ) -> 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: Args:
region (Region): A region that should be visible. region (Region): A region that should be visible.
@@ -638,9 +641,7 @@ class Widget(DOMNode):
bool: True if the window was scrolled. bool: True if the window was scrolled.
""" """
scroll_x, scroll_y = self.scroll_offset window = self.region.at_offset(self.scroll_offset)
width, height = self.region.size
window = Region(scroll_x, scroll_y, width, height)
if spacing is not None: if spacing is not None:
window = window.shrink(spacing) window = window.shrink(spacing)
@@ -650,13 +651,13 @@ class Widget(DOMNode):
window_left, window_top, window_right, window_bottom = window.corners window_left, window_top, window_right, window_bottom = window.corners
left, top, right, bottom = region.corners left, top, right, bottom = region.corners
delta_x = delta_y = 0 delta_x = delta_y = 0
if not ( if not (
(window_right > left >= window_left) (window_right > left >= window_left)
and (window_right > right >= 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( delta_x = min(
left - window_left, left - window_left,
left - (window_right - region.width), left - (window_right - region.width),
@@ -667,6 +668,7 @@ class Widget(DOMNode):
(window_bottom > top >= window_top) (window_bottom > top >= window_top)
and (window_bottom > bottom >= 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( delta_y = min(
top - window_top, top - window_top,
top - (window_bottom - region.height), top - (window_bottom - region.height),

View File

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

View File

@@ -126,7 +126,7 @@ def test_region_from_union():
def test_region_from_origin(): 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(): def test_region_area():
@@ -140,7 +140,7 @@ def test_region_size():
def test_region_origin(): 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(): def test_region_bottom_left():
@@ -275,7 +275,7 @@ def test_region_y_range():
def test_region_reset_origin(): 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(): def test_region_expand():