Skip to content
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

CSS Transition rubber bands when %'s are used #2940

Open
epi052 opened this issue Jul 14, 2023 · 11 comments
Open

CSS Transition rubber bands when %'s are used #2940

epi052 opened this issue Jul 14, 2023 · 11 comments

Comments

@epi052
Copy link

epi052 commented Jul 14, 2023

Have you checked closed issues? https://github.com/Textualize/textual/issues?q=is%3Aissue+is%3Aclosed

yes

Please give a brief but clear explanation of the issue. If you can, include a complete working example that demonstrates the bug. Check it can run without modifications.

current: when using the transition css property on width with a percentage-based start/stop, the animation is jerky/rubber-bands. When moving from larger to smaller, it seems to move past the desired value (maybe to the default widget size?) then snap back to the specified value. When moving from smaller to larger, it appears to start moving from at or near a width of zero, hitting the current width, and then snapping out to the desired width.

expected: transition should look like the textual easing examples when applied to %-based widths.

of note: when using concrete values instead of percents, all seems well.

vokoscreenNG-2023-07-14_06-17-21.mp4

Textual Diagnostics

Versions

Name Value
Textual 0.28.1
Rich 13.4.2

Python

Name Value
Version 3.10.6
Implementation CPython
Compiler GCC 11.3.0
Executable /home/epi/.cache/pypoetry/virtualenvs/ui-overhaul-obYiRisJ-py3.10/bin/python

Operating System

Name Value
System Linux
Release 5.19.0-46-generic
Version #47~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Jun 21 15:35:31 UTC 2

Terminal

Name Value
Terminal Application tmux (3.2a)
TERM xterm-256color
COLORTERM truecolor
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

Name Value
size width=157, height=47
legacy_windows False
min_width 1
max_width 157
is_terminal False
encoding utf-8
max_height 47
justify None
overflow None
no_wrap False
highlight None
markup None
height None

Feel free to add screenshots and / or videos. These can be very helpful!

from textual.app import App, ComposeResult
from textual.widgets import Header, Input, Footer, Button
from textual.containers import Container


class Issue2940(App[None]):

    TITLE = "Test Issue 2940"

    CSS = """
        #my-input {
            width: 20%;
            margin: 1;
            transition: width 500ms in_out_cubic;
        }

        #my-input:focus {
            width: 80%;
        }

        #my-input:blur {
            width: 20%;
        }
    """

    def compose(self) -> ComposeResult:
        yield Header()
        with Container():
            yield Input(id='my-input')
            yield Button('focusable')
        yield Footer()

    def action_toggle(self, bar: str) -> None:
        self.query_one(f"#{bar}bar").toggle_class("shown")


if __name__ == "__main__":
    Issue2940().run()
@github-actions
Copy link

Thank you for your issue. Give us a little time to review it.

PS. You might want to check the FAQ if you haven't done so already.

This is an automated reply, generated by FAQtory

@TomJGooding
Copy link
Contributor

TomJGooding commented Jul 15, 2023

I didn't even know there was a transition CSS property in Textual! I can't find this anywhere in the docs, unless I'm missing something.

I've modified the example hoping it would make it clearer what is happening. This has a custom widget with a slower linear transition, which displays its current width size.

However at the moment I'm still struggling to debug this!

from rich.console import RenderableType
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.widget import Widget
from textual.widgets import Button


class TestWidget(Widget, can_focus=True):
    DEFAULT_CSS = """
    TestWidget {
        background: red;
        height: 3;
        width: 20%;
        transition: width 10000ms linear;
        content-align: center middle;
        text-style: bold;
    }

    TestWidget:focus {
        width: 80%;
        text-style: bold reverse;
    }

    TestWidget:blur {
        width: 20%;
    }
    """

    def render(self) -> RenderableType:
        return str(self.size.width)


class Issue2940(App[None]):
    CSS = """
    Container {
        width: 100;
        height: auto;
        background: $panel;
    }
    """

    def compose(self) -> ComposeResult:
        with Container():
            yield Button("Reset Focus")
            yield TestWidget()


if __name__ == "__main__":
    Issue2940().run()

[EDIT: Forgot I also changed Container width to 100 columns to more easily check percentages]

@TomJGooding
Copy link
Contributor

TomJGooding commented Jul 16, 2023

Ah-ha... If you set the initial width to 20 columns rather than 20%, I think my example shows what is happening.

On focus, the width will shrink until 16 (80% of the widget width of 20), before suddenly jumping to 80 (80% of the container width).

On blur, the width suddenly reduces to 64 (80% of the widget width of 80), then shrinks until 20 (20% of the container width).

@davep
Copy link
Contributor

davep commented Jul 17, 2023

I didn't even know there was a transition CSS property in Textual! I can't find this anywhere in the docs, unless I'm missing something.

@TomJGooding It currently has the status of "undocumented feature".

@epi052
Copy link
Author

epi052 commented Jul 18, 2023

@TomJGooding that's some fine detective work!

@davep the same behavior can be seen when using .animate

from textual.app import App, ComposeResult
from textual.widgets import Header, Input, Footer, Button
from textual.containers import Container


from textual.widgets import Input
from textual.events import Focus, Blur


class AnimatedInput(Input):
    """A custom input widget that animates its width when focused or blurred"""

    DEFAULT_CSS = """
    AnimatedInput {
        height: 3;
        width: 20%;
        background: $surface;
        border: round $panel-lighten-2;
    }

    AnimatedInput:focus {
        border: round $accent;
    }
    """

    def on_focus(self, _event: Focus) -> None:
        """Animates the input width when it is focused.

        This method is called when the input receives focus.
        """
        self.styles.animate(
            "width",
            value="85%",
            duration=0.5,
        )

    def on_blur(self, _event: Blur) -> None:
        """Animates the input width when it loses focus.

        This method is called when the input loses focus.
        """
        self.styles.animate("width", value="20%", duration=0.5)


class Issue2940(App[None]):

    TITLE = "Test Issue 2940"

    def compose(self) -> ComposeResult:
        yield Header()
        with Container():
            yield AnimatedInput()
            yield Button('focusable')
        yield Footer()


if __name__ == "__main__":
    Issue2940().run()```

@davep
Copy link
Contributor

davep commented Jul 18, 2023

@epi052 To be clear: I was simply letting Tom know why he'd failed to find the stylesheet-based version in the docs, not suggesting this wasn't a general issue.

@epi052
Copy link
Author

epi052 commented Jul 18, 2023

@davep I didn't think you were being dismissive. Y'all are insanely helpful. It was more to confirm that it exists in both entry points to that functionality. Should have included it in the original, just wasn't thinking about animate at the time

@TomJGooding
Copy link
Contributor

I might be barking up the wrong tree, but would a maintainer mind explaining what the Scalar.unit and Scalar.percent_unit mean?

I'm probably out of my depth but I'm now invested in trying to solve this!

@willmcgugan
Copy link
Collaborator

percent_unit is context sensitive. If you have width: 50% then percent_unit specifies that the Scalar should be applied to the width.

@TomJGooding
Copy link
Contributor

Thanks Will for clarifying. I think the problem is how the start and destination are 'resolved' here, but after that I'm afraid I'm getting a bit lost

size = widget.outer_size
viewport = widget.app.size
self.start = getattr(styles, attribute).resolve(size, viewport)
self.destination = value.resolve(size, viewport)

@TomJGooding
Copy link
Contributor

TomJGooding commented Jul 24, 2023

Sorry could I also double-check: should the ScalarAnimation start and destination values be the explicit height/width?

If I've understood this correctly, shouldn't size be the parent size, rather than the size of the widget?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants