Skip to content

Commit

Permalink
Merge pull request #608 from Textualize/scroll-to
Browse files Browse the repository at this point in the history
Improvements to scroll_to_widget
  • Loading branch information
willmcgugan authored Jul 13, 2022
2 parents 254dbd6 + 2fd3858 commit 1b6c427
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 86 deletions.
16 changes: 5 additions & 11 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ textual = "textual.cli.cli:run"

[tool.poetry.dependencies]
python = "^3.7"
rich = "^12.4.3"
rich = "^12.5.0"
#rich = {path="../rich", develop=true}
importlib-metadata = "^4.11.3"
typing-extensions = { version = "^4.0.0", python = "<3.8" }
Expand Down
5 changes: 3 additions & 2 deletions sandbox/will/basic.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ DataTable {
}

#header {
color: $text-primary-background-darken-1;
background: $primary-background-darken-1;
color: $text-secondary-background-darken-1;
background: $secondary-background-darken-1;
height: 3;
content-align: center middle;
}
Expand Down Expand Up @@ -109,6 +109,7 @@ Tweet {

.code {
height: auto;

}


Expand Down
24 changes: 24 additions & 0 deletions sandbox/will/buttons.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

Button {
margin: 1;
width: 100%;
}

Vertical {
height: auto;
}

Horizontal {
height: auto;
}

Horizontal Button {
width: 20;

margin: 1 2 ;
}

#scroll {
height: 10;

}
46 changes: 46 additions & 0 deletions sandbox/will/scroll.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from textual import layout, events
from textual.app import App, ComposeResult
from textual.widgets import Button


class ButtonsApp(App[str]):
def compose(self) -> ComposeResult:
yield layout.Vertical(
Button("default", id="foo"),
Button("Where there is a Will"),
Button("There is a Way"),
Button("There can be only one"),
Button.success("success", id="bar"),
layout.Horizontal(
Button("Where there is a Will"),
Button("There is a Way"),
Button("There can be only one"),
Button.warning("warning", id="baz"),
Button("Where there is a Will"),
Button("There is a Way"),
Button("There can be only one"),
id="scroll",
),
Button.error("error", id="baz"),
Button("Where there is a Will"),
Button("There is a Way"),
Button("There can be only one"),
)

def handle_pressed(self, event: Button.Pressed) -> None:
self.app.bell()

async def on_key(self, event: events.Key) -> None:
await self.dispatch_key(event)

def key_d(self):
self.dark = not self.dark


app = ButtonsApp(
log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=2
)

if __name__ == "__main__":
result = app.run()
print(repr(result))
28 changes: 18 additions & 10 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ class ReflowResult(NamedTuple):
class MapGeometry(NamedTuple):
"""Defines the absolute location of a Widget."""

region: Region # The region occupied by the widget
region: Region # The (screen) region occupied by the widget
order: tuple[int, ...] # A tuple of ints defining the painting order
clip: Region # A region to clip the widget by (if a Widget is within a container)
virtual_size: Size # The virtual size (scrollable region) of a widget if it is a container
container_size: Size # The container size (area not occupied by scrollbars)
virtual_region: Region # The region relative to the container (but not necessarily visible)

@property
def visible_region(self) -> Region:
Expand Down Expand Up @@ -271,8 +272,7 @@ def reflow(self, parent: Widget, size: Size) -> ReflowResult:

# Get a map of regions
self.regions = {
widget: (region, clip)
for widget, (region, _order, clip, _, _) in map.items()
widget: (region, clip) for widget, (region, _order, clip, *_) in map.items()
}

# Widgets with changed size
Expand Down Expand Up @@ -326,6 +326,7 @@ def _arrange_root(

def add_widget(
widget: Widget,
virtual_region: Region,
region: Region,
order: tuple[int, ...],
clip: Region,
Expand Down Expand Up @@ -379,6 +380,7 @@ def add_widget(
if sub_widget is not None:
add_widget(
sub_widget,
sub_region,
sub_region + placement_offset,
order + (z,),
sub_clip,
Expand All @@ -394,6 +396,7 @@ def add_widget(
clip,
container_size,
container_size,
chrome_region,
)

map[widget] = MapGeometry(
Expand All @@ -402,16 +405,22 @@ def add_widget(
clip,
total_region.size,
container_size,
virtual_region,
)

else:
# Add the widget to the map
map[widget] = MapGeometry(
region + layout_offset, order, clip, region.size, container_size
region + layout_offset,
order,
clip,
region.size,
container_size,
virtual_region,
)

# Add top level (root) widget
add_widget(root, size.region, (0,), size.region)
add_widget(root, size.region, size.region, (0,), size.region)
return map, widgets

def __iter__(self) -> Iterator[tuple[Widget, Region, Region, Size, Size]]:
Expand All @@ -423,7 +432,7 @@ def __iter__(self) -> Iterator[tuple[Widget, Region, Region, Size, Size]]:
"""
layers = sorted(self.map.items(), key=lambda item: item[1].order, reverse=True)
intersection = Region.intersection
for widget, (region, _order, clip, virtual_size, container_size) in layers:
for widget, (region, _order, clip, virtual_size, container_size, *_) in layers:
yield (
widget,
intersection(region, clip),
Expand Down Expand Up @@ -517,7 +526,7 @@ def cuts(self) -> list[list[int]]:
intersection = Region.intersection
extend = list.extend

for region, order, clip, _, _ in self.map.values():
for region, order, clip, *_ in self.map.values():
region = intersection(region, clip)
if region and (region in screen_region):
x, y, region_width, region_height = region
Expand Down Expand Up @@ -547,13 +556,13 @@ def _get_renders(
overlaps = crop.overlaps
mapped_regions = [
(widget, region, order, clip)
for widget, (region, order, clip, _, _) in self.map.items()
for widget, (region, order, clip, *_) in self.map.items()
if widget.visible and not widget.is_transparent and overlaps(crop)
]
else:
mapped_regions = [
(widget, region, order, clip)
for widget, (region, order, clip, _, _) in self.map.items()
for widget, (region, order, clip, *_) in self.map.items()
if widget.visible and not widget.is_transparent
]

Expand Down Expand Up @@ -594,7 +603,6 @@ def _assemble_chops(
]
return segment_lines

@timer("render")
def render(self, full: bool = False) -> RenderableType | None:
"""Render a layout.
Expand Down
4 changes: 2 additions & 2 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@


DEFAULT_COLORS = ColorSystem(
primary="#406e8e",
primary="#2A4E6E",
secondary="#ffa62b",
warning="#ffa62b",
error="#ba3c5b",
Expand Down Expand Up @@ -645,6 +645,7 @@ def set_focus(self, widget: Widget | None) -> None:
# Change focus
self.focused = widget
# Send focus event
self.screen.scroll_to_widget(widget)
widget.post_message_no_wait(events.Focus(self))
widget.emit_no_wait(events.DescendantFocus(self))

Expand Down Expand Up @@ -926,7 +927,6 @@ def refresh_css(self, animate: bool = True) -> None:
stylesheet.update(self.app, animate=animate)
self.screen._refresh_layout(self.size, full=True)

@timer("_display")
def _display(self, renderable: RenderableType | None) -> None:
"""Display a renderable within a sync.
Expand Down
2 changes: 2 additions & 0 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,10 @@ def add_children(self, *nodes: DOMNode, **named_nodes: DOMNode) -> None:
"""
_append = self.children._append
for node in nodes:
node.set_parent(self)
_append(node)
for node_id, node in named_nodes.items():
node.set_parent(self)
_append(node)
node.id = node_id

Expand Down
21 changes: 20 additions & 1 deletion src/textual/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,8 +569,27 @@ def clip(self, width: int, height: int) -> Region:
)
return new_region

def grow(self, margin: tuple[int, int, int, int]) -> Region:
"""Grow a region by adding spacing.
Args:
margin (Spacing): Defines how many cells to grow the Region by at each edge.
Returns:
Region: New region.
"""

top, right, bottom, left = margin
x, y, width, height = self
return Region(
x=x - left,
y=y - top,
width=max(0, width + left + right),
height=max(0, height + top + bottom),
)

def shrink(self, margin: tuple[int, int, int, int]) -> Region:
"""Shrink a region by pushing each edge inwards.
"""Shrink a region by subtracting spacing.
Args:
margin (Spacing): Defines how many cells to shrink the Region by at each edge.
Expand Down
4 changes: 3 additions & 1 deletion src/textual/layouts/horizontal.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
)
next_x = x + content_width
region = Region(int(x), offset_y, int(next_x - int(x)), int(content_height))
max_height = max(max_height, content_height)
max_height = max(
max_height, content_height + offset_y + box_model.margin.bottom
)
add_placement(WidgetPlacement(region, widget, 0))
x = next_x + margin
max_width = x
Expand Down
Loading

0 comments on commit 1b6c427

Please sign in to comment.