Textualize / textual

The lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.
https://textual.textualize.io/
MIT License
25.77k stars 795 forks source link

CSS Inheritance with Nested Pseudo-Selectors #5206

Open ecoue opened 3 weeks ago

ecoue commented 3 weeks ago

Report

When extending Textual's Input widget with custom CSS using nested selectors, child classes do not properly inherit all CSS rules, specifically those within pseudo-selectors and modifier classes (:hover is working). The issue manifests when using the nested & syntax for pseudo-selectors and modifiers. However, the same CSS rules work correctly when written with non-nested syntax.

Reproduction

from textual.widgets import Input as BaseInput

class Input(BaseInput):
    DEFAULT_CSS = """
    Input {
        color: $text;
        background: $primary-lighten-1;
        border: none;
        width: 1fr;
        height: auto;
        padding: 1 2;

        &:focus {
            border: none;
        }

        &:hover {
            background: $primary-lighten-2;
        }

        &.-invalid {
            background: $error-lighten-1;
            border: none;

            &:focus {
                border: none;
            }

            &:hover {
                background: $error-lighten-2;
            }
        }
    }
    """

class InlineInput(Input):
    DEFAULT_CSS = """
    InlineInput {
        padding: 0 1;
    }
    """

Expectation

Behavior

class Input(BaseInput):
    DEFAULT_CSS = """
    Input {
        color: $text;
        background: $primary-lighten-1;
        border: none;
        width: 1fr;
        height: auto;
        padding: 1 2;
    }

    Input:focus {
        border: none;
    }

    Input:hover {
        background: $primary-lighten-2;
    }

    Input.-invalid {
        background: $error-lighten-1;
        border: none;
    }

    Input.-invalid:focus {
        border: none;
    }

    Input.-invalid:hover {
        background: $error-lighten-2;
    }
    """

Diagnostics

Versions

Name Value
Textual 0.73.0
Rich 13.7.1

Python

Name Value
Version 3.12.4
Implementation CPython
Compiler Clang 15.0.0 (clang-1500.3.9.4)
Executable /Users/.../env/bin/python

Operating System

Name Value
System Darwin
Release 23.6.0
Version Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:21 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T8103

Terminal

Name Value
Terminal Application iTerm.app (3.5.5)
TERM xterm-256color
COLORTERM truecolor
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

Name Value
size width=132, height=41
legacy_windows False
min_width 1
max_width 132
is_terminal True
encoding utf-8
max_height 41
justify None
overflow None
no_wrap False
highlight None
markup None
height None
willmcgugan commented 3 weeks ago

The type in the CSS is the name of the class when it was defined. By styling Input you are clashing with the builtin Input, and the usual specificity rules apply.

To create a custom widget, you will need to create a new name (don't call it Input again) and style that.

class MyInput(Input):
    DEFAULT_CSS = """
    MyWidget {
       ...
    }
    """

"""

This will ensure that the rules you add in MyInput take precedence over the base class Input.

If you need further help, please include a fully working MRE as requested...

If you can, include a complete working example that demonstrates the bug. Check it can run without modifications.

ecoue commented 3 weeks ago

Thank you very much for the quick reply. I had already suspected that the problem could be a naming conflict. Unfortunately, the problem exists independently of this. Rather, it is noticeable that there are no problems (even when named as Input) if no nested CSS is used. This should not produce a different result in any case since it is just syntactic sugar. Furthermore, the problem only exists with the additional subclass InlineInput; Input, on the other hand, works.

willmcgugan commented 3 weeks ago

I will need a fully working example to assist you further.

ecoue commented 3 weeks ago
from textual.app import App
from textual.widgets import Input

class MyInputNested(Input):
    DEFAULT_CSS = """
    MyInputNested {
        border: none;
        padding: 1 2;
        margin-bottom: 1;

        &:focus {
            border: none;
        }
    }
    """

class MyInputNestedSubclass(MyInputNested):
    DEFAULT_CSS = """
    MyInputNestedSubclass {
        padding: 0 2;
        height: auto;
    }
    """

class MyInput(Input):
    DEFAULT_CSS = """
    MyInput {
        border: none;
        padding: 1 2;
        margin-bottom: 1;
    }

    MyInput:focus {
        border: none;
    }
    """

class MyInputSubclass(MyInput):
    DEFAULT_CSS = """
    MyInputSubclass {
        padding: 0 2;
        height: auto;
    }
    """

class BugDemo(App):
    def compose(self):
        yield MyInputNested(placeholder="MyInputNested")
        yield MyInputNestedSubclass(placeholder="MyInputNestedSubclass")

        yield MyInput(placeholder="MyInput")
        yield MyInputSubclass(placeholder="MyInputSubclass")

if __name__ == "__main__":
    app = BugDemo()
    app.run()
ecoue commented 3 weeks ago

Oddly the nested version only stops working if you add an instance of its subclass to the app.

TomJGooding commented 3 weeks ago

You're running quite an old version of Textual, have you tried upgrading? Because I can't seem to reproduce this with the latest version.

ecoue commented 3 weeks ago

You are absolutely right, should have checked that first. The bug seems to be fixed with the current version.