-
Notifications
You must be signed in to change notification settings - Fork 815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix for percentage dimensions #4037
Conversation
There's something wrong with this PR. If you look at |
@rodrigogiraoserrao Could you please isolate a single case that isn't working. i.e. an MRE with as few widgets as possible which displays incorrect behaviour. |
Sure, here is one: from textual.app import App, ComposeResult
from textual.widgets import Label
class NestedCSSTokenErrorApp(App[None]):
CSS = """
Label.target {
width: 100%;
margin: 2;
border: solid red;
}
"""
def compose(self) -> ComposeResult:
# yield Label("1234567890" * 80)
yield Label("", classes="target")
if __name__ == "__main__":
NestedCSSTokenErrorApp().run() Another manifestation of the same problem can be seen in the app below. The bottom label is correctly set to 50% width (via from textual.app import App, ComposeResult
from textual.widgets import Label, Static
class NestedCSSTokenErrorApp(App[None]):
CSS = """
Label {
border: solid red;
margin: 0 4;
}
#first { width: 50%; }
#second {
width: 100%;
max-width: 50%;
}
"""
def compose(self) -> ComposeResult:
yield Static("1234567890" * 8)
yield Label("", id="first")
yield Label("", id="second")
if __name__ == "__main__":
NestedCSSTokenErrorApp().run() |
This may be why you feel my PR changed the semantics of When we talked, you mentioned there was some special case in the code to do something cheeky with |
It's behaving as I'd expect. The percentage applies to the available width, which doesn't include margin. If it didn't behave like this, and you set width 100% with a margin, it would be pushed off screen. It would be simpler (for us) if the percentage was an exact percentage of the container, but this would make it very hard for the dev to achieve sensible layouts. The behaviour should be identical if you have a width of |
So, you're telling me that if I set I know we don't blindly copy browser CSS but it appears that's what it does as well: When we talked, we understood that the effect you're describing could be achieved by setting padding in the container and child dimensions to |
Hope you don't mind me sticking my nose in as usual, but this behaviour with margin seems counter-intuitive to me? Percent size with padding ✅ Here's a simple example with a 10x10 container with a widget of 50% height/width. The widget takes up a quarter of the container size as expected, so this PR fixes the issue with how size was calculated with padding/border. Codefrom textual.app import App, ComposeResult
from textual.containers import Container
from textual.widgets import Static
class ExampleApp(App):
CSS = """
Screen {
align: center middle;
}
Container {
height: 10;
width: 10;
background: blue;
}
Static {
height: 50%;
width: 50%;
background: red;
padding: 2 1;
}
"""
def compose(self) -> ComposeResult:
with Container():
yield Static("5x5")
if __name__ == "__main__":
app = ExampleApp()
app.run() Percent size with padding and margin ❌ But now let's add a margin of 5 (i.e. 50% of the container) to the top and left of the widget. You would expect the widget to now take up the bottom-right quarter of the container, but instead you get this: Codefrom textual.app import App, ComposeResult
from textual.containers import Container
from textual.widgets import Static
class ExampleApp(App):
CSS = """
Screen {
align: center middle;
}
Container {
height: 10;
width: 10;
background: blue;
}
Static {
height: 50%;
width: 50%;
background: red;
padding: 2 1;
margin-top: 5;
margin-left: 5;
}
"""
def compose(self) -> ComposeResult:
with Container():
yield Static("5x5")
if __name__ == "__main__":
app = ExampleApp()
app.run() |
It's a pragmatic design choice for Textual. If I have a widget with width and height 100% and apply a margin, it currently produces something useful. i.e. the widget is centered in the screen. If the margin was additive, then the widget would overflow the screen boundary. That is very unlikely to be useful for the developer. If you do want a widget to be an exact percentage, but offset from the top left then you can use The interaction with margin is orthogonal to the original issue. The original issue was that percentage dimensions excluded the gutter, which this PR should fix. Nothing re margins has changed. We can always change that if needed, but AFAIK nobody as found this to be a problem, and I think we should err on the side of not breaking things. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If margin
+ %
is to work like so, shouldn't margin
be taken into account for max
/min
units, then?
E.g., regardless of the way we decide to resolve %
units with margin, I'd expect the two placeholders below to have the same width because of their TCSS:
from textual.app import App, ComposeResult
from textual.widgets import Label, Placeholder
class NestedCSSTokenErrorApp(App[None]):
CSS = """
Placeholder {
margin: 0 4;
height: 3;
}
#first {
width: 50%; # <--
}
#second {
width: 999; # <-- this is huge...
max-width: 50%; # <-- ... so, this one kicks in.
}
"""
def compose(self) -> ComposeResult:
yield Label("1234567890" * 8)
yield Placeholder("", id="first")
yield Placeholder("", id="second")
if __name__ == "__main__":
NestedCSSTokenErrorApp().run()
box_margins: list[Spacing] = [ | ||
styles.margin for styles in child_styles if styles.overlay != "screen" | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you changed this because you probably thought of an edge case that wasn't being covered; wouldn't this warrant a test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably should. Going to leave it for now, because overlay isn't yet documented.
max( | ||
min( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment re: test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is covered by a snapshot, but it probably does deserve a more specific test. Going to leave it for now though.
max( | ||
min( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment re: test
@willmcgugan you didn't say anything regarding this comment so I'm not sure if this slipped past you or if it's the intended behaviour. |
Calculating percentage dimensions didn't take in to account padding / border.
Fixes #3721
Supersedes #4023