Skip to content

Commit a465f5c

Browse files
authored
Horizontal width auto (#1155)
* Improvements to width:auto HorizontalLayout * Fix HorizontalLayout.get_content_width * Horizontal width auto improvement * Removing some printxz * Update snapshot for horizontal layout width auto dock
1 parent a37eac3 commit a465f5c

File tree

6 files changed

+243
-58
lines changed

6 files changed

+243
-58
lines changed

src/textual/layouts/horizontal.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,12 @@ def get_content_width(self, widget: Widget, container: Size, viewport: Size) ->
7272
Returns:
7373
int: Width of the content.
7474
"""
75-
width: int | None = None
76-
gutter_width = widget.gutter.width
77-
for child in widget.displayed_children:
78-
if not child.is_container:
79-
child_width = (
80-
child.get_content_width(container, viewport)
81-
+ gutter_width
82-
+ child.gutter.width
83-
)
84-
if width is None:
85-
width = child_width
86-
else:
87-
width += child_width
88-
if width is None:
75+
if not widget.displayed_children:
8976
width = container.width
90-
77+
else:
78+
placements, *_ = widget._arrange(container)
79+
width = max(
80+
placement.region.right + placement.margin.right
81+
for placement in placements
82+
)
9183
return width

tests/layouts/test_horizontal.py

Lines changed: 26 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,29 @@
1-
from textual.geometry import Size
2-
from textual.layouts.horizontal import HorizontalLayout
3-
from textual.widget import Widget
4-
5-
6-
class SizedWidget(Widget):
7-
"""Simple Widget wrapped allowing you to modify the return values for
8-
get_content_width and get_content_height via the constructor."""
9-
10-
def __init__(
11-
self,
12-
*children: Widget,
13-
content_width: int = 10,
14-
content_height: int = 5,
15-
):
16-
super().__init__(*children)
17-
self.content_width = content_width
18-
self.content_height = content_height
19-
20-
def get_content_width(self, container: Size, viewport: Size) -> int:
21-
return self.content_width
1+
import pytest
222

23-
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
24-
return self.content_height
25-
26-
27-
CHILDREN = [
28-
SizedWidget(content_width=10, content_height=5),
29-
SizedWidget(content_width=4, content_height=2),
30-
SizedWidget(content_width=12, content_height=3),
31-
]
32-
33-
34-
def test_horizontal_get_content_width():
35-
parent = Widget(*CHILDREN)
36-
layout = HorizontalLayout()
37-
width = layout.get_content_width(widget=parent, container=Size(), viewport=Size())
38-
assert width == sum(child.content_width for child in CHILDREN)
3+
from textual.app import App, ComposeResult
4+
from textual.containers import Horizontal
5+
from textual.widget import Widget
396

407

41-
def test_horizontal_get_content_width_no_children():
42-
parent = Widget()
43-
layout = HorizontalLayout()
44-
container_size = Size(24, 24)
45-
width = layout.get_content_width(widget=parent, container=container_size, viewport=Size())
46-
assert width == container_size.width
8+
@pytest.fixture
9+
async def app():
10+
class HorizontalAutoWidth(App):
11+
def compose(self) -> ComposeResult:
12+
child1 = Widget(id="child1")
13+
child1.styles.width = 4
14+
child2 = Widget(id="child2")
15+
child2.styles.width = 6
16+
child3 = Widget(id="child3")
17+
child3.styles.width = 5
18+
self.horizontal = Horizontal(child1, child2, child3)
19+
yield self.horizontal
20+
21+
app = HorizontalAutoWidth()
22+
async with app.run_test():
23+
yield app
24+
25+
26+
async def test_horizontal_get_content_width(app):
27+
size = app.screen.size
28+
width = app.horizontal.get_content_width(size, size)
29+
assert width == 15

tests/snapshot_tests/__snapshots__/test_snapshots.ambr

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5849,6 +5849,164 @@
58495849

58505850
'''
58515851
# ---
5852+
# name: test_horizontal_layout_width_auto_dock
5853+
'''
5854+
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
5855+
<!-- Generated with Rich https://www.textualize.io -->
5856+
<style>
5857+
5858+
@font-face {
5859+
font-family: "Fira Code";
5860+
src: local("FiraCode-Regular"),
5861+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
5862+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
5863+
font-style: normal;
5864+
font-weight: 400;
5865+
}
5866+
@font-face {
5867+
font-family: "Fira Code";
5868+
src: local("FiraCode-Bold"),
5869+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
5870+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
5871+
font-style: bold;
5872+
font-weight: 700;
5873+
}
5874+
5875+
.terminal-3689181897-matrix {
5876+
font-family: Fira Code, monospace;
5877+
font-size: 20px;
5878+
line-height: 24.4px;
5879+
font-variant-east-asian: full-width;
5880+
}
5881+
5882+
.terminal-3689181897-title {
5883+
font-size: 18px;
5884+
font-weight: bold;
5885+
font-family: arial;
5886+
}
5887+
5888+
.terminal-3689181897-r1 { fill: #e1f0ff }
5889+
.terminal-3689181897-r2 { fill: #c5c8c6 }
5890+
.terminal-3689181897-r3 { fill: #e1e1e1 }
5891+
.terminal-3689181897-r4 { fill: #ebf0e2 }
5892+
.terminal-3689181897-r5 { fill: #f7e0ef }
5893+
</style>
5894+
5895+
<defs>
5896+
<clipPath id="terminal-3689181897-clip-terminal">
5897+
<rect x="0" y="0" width="975.0" height="584.5999999999999" />
5898+
</clipPath>
5899+
<clipPath id="terminal-3689181897-line-0">
5900+
<rect x="0" y="1.5" width="976" height="24.65"/>
5901+
</clipPath>
5902+
<clipPath id="terminal-3689181897-line-1">
5903+
<rect x="0" y="25.9" width="976" height="24.65"/>
5904+
</clipPath>
5905+
<clipPath id="terminal-3689181897-line-2">
5906+
<rect x="0" y="50.3" width="976" height="24.65"/>
5907+
</clipPath>
5908+
<clipPath id="terminal-3689181897-line-3">
5909+
<rect x="0" y="74.7" width="976" height="24.65"/>
5910+
</clipPath>
5911+
<clipPath id="terminal-3689181897-line-4">
5912+
<rect x="0" y="99.1" width="976" height="24.65"/>
5913+
</clipPath>
5914+
<clipPath id="terminal-3689181897-line-5">
5915+
<rect x="0" y="123.5" width="976" height="24.65"/>
5916+
</clipPath>
5917+
<clipPath id="terminal-3689181897-line-6">
5918+
<rect x="0" y="147.9" width="976" height="24.65"/>
5919+
</clipPath>
5920+
<clipPath id="terminal-3689181897-line-7">
5921+
<rect x="0" y="172.3" width="976" height="24.65"/>
5922+
</clipPath>
5923+
<clipPath id="terminal-3689181897-line-8">
5924+
<rect x="0" y="196.7" width="976" height="24.65"/>
5925+
</clipPath>
5926+
<clipPath id="terminal-3689181897-line-9">
5927+
<rect x="0" y="221.1" width="976" height="24.65"/>
5928+
</clipPath>
5929+
<clipPath id="terminal-3689181897-line-10">
5930+
<rect x="0" y="245.5" width="976" height="24.65"/>
5931+
</clipPath>
5932+
<clipPath id="terminal-3689181897-line-11">
5933+
<rect x="0" y="269.9" width="976" height="24.65"/>
5934+
</clipPath>
5935+
<clipPath id="terminal-3689181897-line-12">
5936+
<rect x="0" y="294.3" width="976" height="24.65"/>
5937+
</clipPath>
5938+
<clipPath id="terminal-3689181897-line-13">
5939+
<rect x="0" y="318.7" width="976" height="24.65"/>
5940+
</clipPath>
5941+
<clipPath id="terminal-3689181897-line-14">
5942+
<rect x="0" y="343.1" width="976" height="24.65"/>
5943+
</clipPath>
5944+
<clipPath id="terminal-3689181897-line-15">
5945+
<rect x="0" y="367.5" width="976" height="24.65"/>
5946+
</clipPath>
5947+
<clipPath id="terminal-3689181897-line-16">
5948+
<rect x="0" y="391.9" width="976" height="24.65"/>
5949+
</clipPath>
5950+
<clipPath id="terminal-3689181897-line-17">
5951+
<rect x="0" y="416.3" width="976" height="24.65"/>
5952+
</clipPath>
5953+
<clipPath id="terminal-3689181897-line-18">
5954+
<rect x="0" y="440.7" width="976" height="24.65"/>
5955+
</clipPath>
5956+
<clipPath id="terminal-3689181897-line-19">
5957+
<rect x="0" y="465.1" width="976" height="24.65"/>
5958+
</clipPath>
5959+
<clipPath id="terminal-3689181897-line-20">
5960+
<rect x="0" y="489.5" width="976" height="24.65"/>
5961+
</clipPath>
5962+
<clipPath id="terminal-3689181897-line-21">
5963+
<rect x="0" y="513.9" width="976" height="24.65"/>
5964+
</clipPath>
5965+
<clipPath id="terminal-3689181897-line-22">
5966+
<rect x="0" y="538.3" width="976" height="24.65"/>
5967+
</clipPath>
5968+
</defs>
5969+
5970+
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-3689181897-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">HorizontalAutoWidth</text>
5971+
<g transform="translate(26,22)">
5972+
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
5973+
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
5974+
<circle cx="44" cy="0" r="7" fill="#28c840"/>
5975+
</g>
5976+
5977+
<g transform="translate(9, 41)" clip-path="url(#terminal-3689181897-clip-terminal)">
5978+
<rect fill="#1e90ff" x="0" y="1.5" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#483d8b" x="61" y="1.5" width="536.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="597.8" y="1.5" width="378.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e90ff" x="0" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e90ff" x="24.4" y="25.9" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#483d8b" x="61" y="25.9" width="268.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#6b8e23" x="329.4" y="25.9" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#6b8e23" x="427" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#483d8b" x="451.4" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#6b8e23" x="463.6" y="25.9" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#6b8e23" x="561.2" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#483d8b" x="585.6" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="597.8" y="25.9" width="378.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e90ff" x="0" y="50.3" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#483d8b" x="61" y="50.3" width="536.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="597.8" y="50.3" width="378.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e90ff" x="0" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e90ff" x="12.2" y="74.7" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#c71585" x="36.6" y="74.7" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#c71585" x="61" y="74.7" width="134.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#c71585" x="195.2" y="74.7" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#483d8b" x="280.6" y="74.7" width="317.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="597.8" y="74.7" width="378.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
5979+
<g class="terminal-3689181897-matrix">
5980+
<text class="terminal-3689181897-r1" x="0" y="20" textLength="61" clip-path="url(#terminal-3689181897-line-0)">Docke</text><text class="terminal-3689181897-r2" x="976" y="20" textLength="12.2" clip-path="url(#terminal-3689181897-line-0)">
5981+
</text><text class="terminal-3689181897-r1" x="0" y="44.4" textLength="24.4" clip-path="url(#terminal-3689181897-line-1)">d&#160;</text><text class="terminal-3689181897-r4" x="329.4" y="44.4" textLength="97.6" clip-path="url(#terminal-3689181897-line-1)">Widget&#160;1</text><text class="terminal-3689181897-r4" x="463.6" y="44.4" textLength="97.6" clip-path="url(#terminal-3689181897-line-1)">Widget&#160;2</text><text class="terminal-3689181897-r2" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-3689181897-line-1)">
5982+
</text><text class="terminal-3689181897-r1" x="0" y="68.8" textLength="61" clip-path="url(#terminal-3689181897-line-2)">left&#160;</text><text class="terminal-3689181897-r2" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-3689181897-line-2)">
5983+
</text><text class="terminal-3689181897-r1" x="0" y="93.2" textLength="12.2" clip-path="url(#terminal-3689181897-line-3)">1</text><text class="terminal-3689181897-r5" x="36.6" y="93.2" textLength="24.4" clip-path="url(#terminal-3689181897-line-3)">Do</text><text class="terminal-3689181897-r5" x="61" y="93.2" textLength="134.2" clip-path="url(#terminal-3689181897-line-3)">cked&#160;left&#160;2</text><text class="terminal-3689181897-r2" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-3689181897-line-3)">
5984+
</text><text class="terminal-3689181897-r2" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-3689181897-line-4)">
5985+
</text><text class="terminal-3689181897-r2" x="976" y="142" textLength="12.2" clip-path="url(#terminal-3689181897-line-5)">
5986+
</text><text class="terminal-3689181897-r2" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-3689181897-line-6)">
5987+
</text><text class="terminal-3689181897-r2" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-3689181897-line-7)">
5988+
</text><text class="terminal-3689181897-r2" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-3689181897-line-8)">
5989+
</text><text class="terminal-3689181897-r2" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-3689181897-line-9)">
5990+
</text><text class="terminal-3689181897-r2" x="976" y="264" textLength="12.2" clip-path="url(#terminal-3689181897-line-10)">
5991+
</text><text class="terminal-3689181897-r2" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-3689181897-line-11)">
5992+
</text><text class="terminal-3689181897-r2" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-3689181897-line-12)">
5993+
</text><text class="terminal-3689181897-r2" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-3689181897-line-13)">
5994+
</text><text class="terminal-3689181897-r2" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-3689181897-line-14)">
5995+
</text><text class="terminal-3689181897-r2" x="976" y="386" textLength="12.2" clip-path="url(#terminal-3689181897-line-15)">
5996+
</text><text class="terminal-3689181897-r2" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-3689181897-line-16)">
5997+
</text><text class="terminal-3689181897-r2" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-3689181897-line-17)">
5998+
</text><text class="terminal-3689181897-r2" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-3689181897-line-18)">
5999+
</text><text class="terminal-3689181897-r2" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-3689181897-line-19)">
6000+
</text><text class="terminal-3689181897-r2" x="976" y="508" textLength="12.2" clip-path="url(#terminal-3689181897-line-20)">
6001+
</text><text class="terminal-3689181897-r2" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-3689181897-line-21)">
6002+
</text><text class="terminal-3689181897-r2" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-3689181897-line-22)">
6003+
</text>
6004+
</g>
6005+
</g>
6006+
</svg>
6007+
6008+
'''
6009+
# ---
58526010
# name: test_input_and_focus
58536011
'''
58546012
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.widget {
2+
background: olivedrab;
3+
width: 10;
4+
margin: 1;
5+
}
6+
7+
#dock-1 {
8+
dock: left;
9+
background: dodgerblue;
10+
width: 5;
11+
}
12+
13+
#dock-2 {
14+
dock: left;
15+
background: mediumvioletred;
16+
margin: 3;
17+
width: 20;
18+
}
19+
20+
#horizontal {
21+
width: auto;
22+
height: auto;
23+
background: darkslateblue;
24+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from textual.app import App, ComposeResult
2+
from textual.containers import Horizontal
3+
from textual.widgets import Static
4+
5+
6+
class HorizontalAutoWidth(App):
7+
"""
8+
Checks that the auto width of the parent Horizontal is correct.
9+
"""
10+
def compose(self) -> ComposeResult:
11+
yield Horizontal(
12+
Static("Docked left 1", id="dock-1"),
13+
Static("Docked left 2", id="dock-2"),
14+
Static("Widget 1", classes="widget"),
15+
Static("Widget 2", classes="widget"),
16+
id="horizontal",
17+
)
18+
19+
20+
app = HorizontalAutoWidth(css_path="horizontal_auto_width.css")
21+
if __name__ == '__main__':
22+
app.run()

tests/snapshot_tests/test_snapshots.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import pytest
44

5+
# These paths should be relative to THIS directory.
56
WIDGET_EXAMPLES_DIR = Path("../../docs/examples/widgets")
67
LAYOUT_EXAMPLES_DIR = Path("../../docs/examples/guide/layout")
78
STYLES_EXAMPLES_DIR = Path("../../docs/examples/styles")
9+
SNAPSHOT_APPS_DIR = Path("./snapshot_apps")
810

911

1012
# --- Layout related stuff ---
@@ -29,6 +31,10 @@ def test_horizontal_layout(snap_compare):
2931
assert snap_compare(LAYOUT_EXAMPLES_DIR / "horizontal_layout.py")
3032

3133

34+
def test_horizontal_layout_width_auto_dock(snap_compare):
35+
assert snap_compare(SNAPSHOT_APPS_DIR / "horizontal_auto_width.py")
36+
37+
3238
def test_vertical_layout(snap_compare):
3339
assert snap_compare(LAYOUT_EXAMPLES_DIR / "vertical_layout.py")
3440

0 commit comments

Comments
 (0)