mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
allow scrollbars to be customized
This commit is contained in:
@@ -7,6 +7,13 @@
|
||||
}
|
||||
*/
|
||||
|
||||
* {
|
||||
scrollbar-background: $panel-darken-2;
|
||||
scrollbar-background-hover: $panel-darken-3;
|
||||
scrollbar-color: $system;
|
||||
scrollbar-color-active: $accent-darken-1;
|
||||
}
|
||||
|
||||
App > Screen {
|
||||
layout: dock;
|
||||
docks: side=left/1;
|
||||
@@ -31,7 +38,7 @@ App > Screen {
|
||||
#sidebar .title {
|
||||
height: 3;
|
||||
background: $primary-darken-2;
|
||||
color: $text-primary-darken-2;
|
||||
color: $text-primary-darken-2 ;
|
||||
border-right: outer $primary-darken-3;
|
||||
}
|
||||
|
||||
@@ -59,36 +66,42 @@ App > Screen {
|
||||
color: $text-background;
|
||||
background: $background;
|
||||
layout: vertical;
|
||||
overflow-y:scroll;
|
||||
|
||||
}
|
||||
|
||||
|
||||
Tweet {
|
||||
height: 16;
|
||||
max-width: 80;
|
||||
margin: 1 3;
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
layout: vertical;
|
||||
border: outer $accent2;
|
||||
/* border: outer $primary; */
|
||||
padding: 1;
|
||||
|
||||
border: wide $panel-darken-2;
|
||||
overflow-y: scroll
|
||||
}
|
||||
|
||||
|
||||
TweetHeader {
|
||||
height:1
|
||||
background: $accent1
|
||||
color: $text-accent1
|
||||
background: $accent
|
||||
color: $text-accent
|
||||
}
|
||||
|
||||
TweetBody {
|
||||
background: $surface
|
||||
color: $text-surface-fade-1
|
||||
height:6;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
height:20;
|
||||
padding: 0 1 0 0;
|
||||
|
||||
}
|
||||
|
||||
.button {
|
||||
background: $accent2;
|
||||
color: $text-accent2;
|
||||
background: $accent;
|
||||
color: $text-accent;
|
||||
width:20;
|
||||
height: 3
|
||||
border: tall $text-background;
|
||||
@@ -100,8 +113,8 @@ TweetBody {
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: $accent1-darken-1;
|
||||
color: $text-accent1-darken-1;
|
||||
background: $success-darken-1;
|
||||
color: $text-success-darken-1;
|
||||
width: 20;
|
||||
height: 3
|
||||
border: tall $text-background;
|
||||
@@ -112,10 +125,10 @@ TweetBody {
|
||||
}
|
||||
|
||||
#footer {
|
||||
color: $text-accent2;
|
||||
background: $accent2;
|
||||
color: $text-accent;
|
||||
background: $accent;
|
||||
height: 1;
|
||||
border-top: hkey $accent2-darken-2;
|
||||
border-top: hkey $accent-darken-2;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,26 +140,29 @@ OptionItem {
|
||||
height: 3;
|
||||
background: $primary;
|
||||
transition: background 100ms linear;
|
||||
border-right: outer $primary-darken-3;
|
||||
border-right: outer $primary-darken-2;
|
||||
border-left: hidden;
|
||||
}
|
||||
|
||||
OptionItem:hover {
|
||||
height: 3;
|
||||
background: $accent1-darken-2;
|
||||
color: $accent;
|
||||
background: $primary-darken-1;
|
||||
/* border-top: hkey $accent2-darken-3;
|
||||
border-bottom: hkey $accent2-darken-3; */
|
||||
text-style: bold;
|
||||
border-right: outer $accent1-darken-3;
|
||||
border-left: outer $accent-darken-2;
|
||||
}
|
||||
|
||||
Error {
|
||||
max-width: 78;
|
||||
max-width: 80;
|
||||
height:3;
|
||||
background: $error;
|
||||
color: $text-error;
|
||||
border-top: hkey $error-darken-3;
|
||||
border-bottom: hkey $error-darken-3;
|
||||
border-top: hkey $error-darken-2;
|
||||
border-bottom: hkey $error-darken-2;
|
||||
margin: 1 3;
|
||||
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
@@ -155,8 +171,8 @@ Warning {
|
||||
height:3;
|
||||
background: $warning;
|
||||
color: $text-warning-fade-1;
|
||||
border-top: hkey $warning-darken-3;
|
||||
border-bottom: hkey $warning-darken-3;
|
||||
border-top: hkey $warning-darken-2;
|
||||
border-bottom: hkey $warning-darken-2;
|
||||
margin: 1 2;
|
||||
text-style: bold;
|
||||
}
|
||||
@@ -164,10 +180,10 @@ Warning {
|
||||
Success {
|
||||
max-width: 80;
|
||||
height:3;
|
||||
background: $success-lighten-2;
|
||||
color: $text-success-lighten-2-fade-1;
|
||||
border-top: hkey $success-darken-3;
|
||||
border-bottom: hkey $success-darken-3;
|
||||
background: $success-lighten-3;
|
||||
color: $text-success-lighten-3-fade-1;
|
||||
border-top: hkey $success;
|
||||
border-bottom: hkey $success;
|
||||
margin: 1 2;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ from textual.widget import Widget
|
||||
|
||||
|
||||
lorem = Text.from_markup(
|
||||
"""Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """,
|
||||
"""Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """
|
||||
"""Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -115,12 +115,13 @@ class App(DOMNode):
|
||||
|
||||
self.design = ColorSystem(
|
||||
primary="#406e8e", # blueish
|
||||
secondary="#6d9f71", # purplesis
|
||||
secondary="#6d9f71", # purpleish
|
||||
warning="#ffa62b", # orange
|
||||
error="#ba3c5b", # error
|
||||
success="#6d9f71", # green
|
||||
accent1="#ffa62b",
|
||||
accent2="#5a4599",
|
||||
accent="#ffa62b",
|
||||
system="#5a4599"
|
||||
# accent2="#5a4599",
|
||||
)
|
||||
|
||||
self.stylesheet = Stylesheet(variables=self.get_css_variables())
|
||||
|
||||
@@ -237,7 +237,7 @@ class Color(NamedTuple):
|
||||
|
||||
@classmethod
|
||||
@lru_cache(maxsize=1024 * 4)
|
||||
def parse(cls, color_text: str) -> Color:
|
||||
def parse(cls, color_text: str | Color) -> Color:
|
||||
"""Parse a string containing a CSS-style color.
|
||||
|
||||
Args:
|
||||
@@ -249,7 +249,8 @@ class Color(NamedTuple):
|
||||
Returns:
|
||||
Color: New color object.
|
||||
"""
|
||||
|
||||
if isinstance(color_text, Color):
|
||||
return color_text
|
||||
ansi_color = ANSI_COLOR_TO_RGB.get(color_text)
|
||||
if ansi_color is not None:
|
||||
return cls(*ansi_color)
|
||||
|
||||
@@ -625,7 +625,7 @@ class NameListProperty:
|
||||
def __get__(
|
||||
self, obj: StylesBase, objtype: type[StylesBase] | None = None
|
||||
) -> tuple[str, ...]:
|
||||
return obj.get_rule(self.name, ())
|
||||
return cast(tuple[str, ...], obj.get_rule(self.name, ()))
|
||||
|
||||
def __set__(self, obj: StylesBase, names: str | tuple[str] | None = None):
|
||||
|
||||
@@ -645,13 +645,15 @@ class NameListProperty:
|
||||
class ColorProperty:
|
||||
"""Descriptor for getting and setting color properties."""
|
||||
|
||||
def __init__(self, default_color: Color) -> None:
|
||||
self._default_color = default_color
|
||||
def __init__(self, default_color: Color | str) -> None:
|
||||
self._default_color = Color.parse(default_color)
|
||||
|
||||
def __set_name__(self, owner: StylesBase, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj: StylesBase, objtype: type[Styles] | None = None) -> Color:
|
||||
def __get__(
|
||||
self, obj: StylesBase, objtype: type[StylesBase] | None = None
|
||||
) -> Color:
|
||||
"""Get a ``Color``.
|
||||
|
||||
Args:
|
||||
@@ -661,7 +663,7 @@ class ColorProperty:
|
||||
Returns:
|
||||
Color: The Color
|
||||
"""
|
||||
return obj.get_rule(self.name, self._default_color)
|
||||
return cast(Color, obj.get_rule(self.name, self._default_color))
|
||||
|
||||
def __set__(self, obj: StylesBase, color: Color | str | None):
|
||||
"""Set the Color
|
||||
|
||||
@@ -460,10 +460,11 @@ class StylesBuilder:
|
||||
)
|
||||
|
||||
def process_color(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
name = name.replace("-", "_")
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self.styles._rules["color"] = Color.parse(token.value)
|
||||
self.styles._rules[name] = Color.parse(token.value)
|
||||
except Exception as error:
|
||||
self.error(
|
||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||
@@ -476,18 +477,37 @@ class StylesBuilder:
|
||||
def process_background(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self.styles._rules["background"] = Color.parse(token.value)
|
||||
except Exception as error:
|
||||
self.error(
|
||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||
)
|
||||
else:
|
||||
self.error(
|
||||
name, token, f"unexpected token {token.value!r} in declaration"
|
||||
)
|
||||
self.process_color(name, tokens, important)
|
||||
|
||||
def process_scrollbar_color(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self.process_color(name, tokens, important)
|
||||
|
||||
def process_scrollbar_color_hover(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self.process_color(name, tokens, important)
|
||||
|
||||
def process_scrollbar_color_active(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self.process_color(name, tokens, important)
|
||||
|
||||
def process_scrollbar_background(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self.process_color(name, tokens, important)
|
||||
|
||||
def process_scrollbar_background_hover(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self.process_color(name, tokens, important)
|
||||
|
||||
def process_scrollbar_background_active(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
self.process_color(name, tokens, important)
|
||||
|
||||
def process_text_style(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
from .model import CombinatorType, Selector, SelectorSet, SelectorType
|
||||
from .model import CombinatorType, Selector, SelectorSet
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -28,14 +28,17 @@ class CombinatorType(Enum):
|
||||
CHILD = 3
|
||||
|
||||
|
||||
@dataclass
|
||||
class Location:
|
||||
line: tuple[int, int]
|
||||
column: tuple[int, int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Selector:
|
||||
"""Represents a CSS selector.
|
||||
|
||||
Some examples of selectors:
|
||||
|
||||
*
|
||||
Header.title
|
||||
App > Content
|
||||
"""
|
||||
|
||||
name: str
|
||||
combinator: CombinatorType = CombinatorType.DESCENDENT
|
||||
type: SelectorType = SelectorType.TYPE
|
||||
@@ -46,6 +49,7 @@ class Selector:
|
||||
|
||||
@property
|
||||
def css(self) -> str:
|
||||
"""Rebuilds the selector as it would appear in CSS."""
|
||||
pseudo_suffix = "".join(f":{name}" for name in self.pseudo_classes)
|
||||
if self.type == SelectorType.UNIVERSAL:
|
||||
return "*"
|
||||
|
||||
@@ -108,6 +108,14 @@ class RulesMap(TypedDict, total=False):
|
||||
|
||||
transitions: dict[str, Transition]
|
||||
|
||||
scrollbar_color: Color
|
||||
scrollbar_color_hover: Color
|
||||
scrollbar_color_active: Color
|
||||
|
||||
scrollbar_background: Color
|
||||
scrollbar_background_hover: Color
|
||||
scrollbar_background_active: Color
|
||||
|
||||
|
||||
RULE_NAMES = list(RulesMap.__annotations__.keys())
|
||||
_rule_getter = attrgetter(*RULE_NAMES)
|
||||
@@ -134,6 +142,12 @@ class StylesBase(ABC):
|
||||
"max_height",
|
||||
"color",
|
||||
"background",
|
||||
"scrollbar_color",
|
||||
"scrollbar_color_hover",
|
||||
"scrollbar_color_active",
|
||||
"scrollbar_background",
|
||||
"scrollbar_background_hover",
|
||||
"scrollbar_background_active",
|
||||
}
|
||||
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||
@@ -182,6 +196,14 @@ class StylesBase(ABC):
|
||||
|
||||
rich_style = StyleProperty()
|
||||
|
||||
scrollbar_color = ColorProperty("bright_magenta")
|
||||
scrollbar_color_hover = ColorProperty("yellow")
|
||||
scrollbar_color_active = ColorProperty("bright_yellow")
|
||||
|
||||
scrollbar_background = ColorProperty("#555555")
|
||||
scrollbar_background_hover = ColorProperty("#444444")
|
||||
scrollbar_background_active = ColorProperty("black")
|
||||
|
||||
def __eq__(self, styles: object) -> bool:
|
||||
"""Check that Styles containts the same rules."""
|
||||
if not isinstance(styles, StylesBase):
|
||||
|
||||
@@ -11,10 +11,13 @@ from .color import Color, WHITE
|
||||
|
||||
NUMBER_OF_SHADES = 3
|
||||
|
||||
# Where no content exists
|
||||
DEFAULT_DARK_BACKGROUND = "#000000"
|
||||
# What text usually goes on top off
|
||||
DEFAULT_DARK_SURFACE = "#121212"
|
||||
DEFAULT_LIGHT_BACKGROUND = "#f5f5f5"
|
||||
DEFAULT_LIGHT_SURFACE = "#efefef"
|
||||
|
||||
DEFAULT_LIGHT_SURFACE = "#f5f5f5"
|
||||
DEFAULT_LIGHT_BACKGROUND = "#efefef"
|
||||
|
||||
|
||||
class ColorProperty:
|
||||
@@ -56,8 +59,9 @@ class ColorSystem:
|
||||
"warning",
|
||||
"error",
|
||||
"success",
|
||||
"accent1",
|
||||
"accent2",
|
||||
"accent",
|
||||
"panel",
|
||||
"system",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -67,20 +71,22 @@ class ColorSystem:
|
||||
warning: str | None = None,
|
||||
error: str | None = None,
|
||||
success: str | None = None,
|
||||
accent1: str | None = None,
|
||||
accent2: str | None = None,
|
||||
accent: str | None = None,
|
||||
system: str | None = None,
|
||||
background: str | None = None,
|
||||
surface: str | None = None,
|
||||
panel: str | None = None,
|
||||
):
|
||||
self._primary = primary
|
||||
self._secondary = secondary
|
||||
self._warning = warning
|
||||
self._error = error
|
||||
self._success = success
|
||||
self._accent1 = accent1
|
||||
self._accent2 = accent2
|
||||
self._accent = accent
|
||||
self._system = system
|
||||
self._background = background
|
||||
self._surface = surface
|
||||
self._panel = panel
|
||||
|
||||
@property
|
||||
def primary(self) -> Color:
|
||||
@@ -91,10 +97,11 @@ class ColorSystem:
|
||||
warning = ColorProperty()
|
||||
error = ColorProperty()
|
||||
success = ColorProperty()
|
||||
accent1 = ColorProperty()
|
||||
accent2 = ColorProperty()
|
||||
accent = ColorProperty()
|
||||
system = ColorProperty()
|
||||
surface = ColorProperty()
|
||||
background = ColorProperty()
|
||||
panel = ColorProperty()
|
||||
|
||||
@property
|
||||
def shades(self) -> Iterable[str]:
|
||||
@@ -132,8 +139,8 @@ class ColorSystem:
|
||||
warning = self.warning or primary
|
||||
error = self.error or secondary
|
||||
success = self.success or secondary
|
||||
accent1 = self.accent1 or primary
|
||||
accent2 = self.accent2 or secondary
|
||||
accent = self.accent or primary
|
||||
system = self.system or accent
|
||||
|
||||
background = self.background or Color.parse(
|
||||
DEFAULT_DARK_BACKGROUND if dark else DEFAULT_LIGHT_BACKGROUND
|
||||
@@ -143,6 +150,15 @@ class ColorSystem:
|
||||
DEFAULT_DARK_SURFACE if dark else DEFAULT_LIGHT_SURFACE
|
||||
)
|
||||
|
||||
text = background.get_contrast_text(1.0)
|
||||
if self.panel is None:
|
||||
if dark:
|
||||
panel = background.blend(text, luminosity_spread)
|
||||
else:
|
||||
panel = background.blend(text, luminosity_spread / 2)
|
||||
else:
|
||||
panel = self.panel
|
||||
|
||||
colors: dict[str, str] = {}
|
||||
|
||||
def luminosity_range(spread) -> Iterable[tuple[str, float]]:
|
||||
@@ -167,12 +183,13 @@ class ColorSystem:
|
||||
("primary", primary),
|
||||
("secondary", secondary),
|
||||
("background", background),
|
||||
("panel", panel),
|
||||
("surface", surface),
|
||||
("warning", warning),
|
||||
("error", error),
|
||||
("success", success),
|
||||
("accent1", accent1),
|
||||
("accent2", accent2),
|
||||
("accent", accent),
|
||||
("system", system),
|
||||
]
|
||||
|
||||
# Colors names that have a dark variant
|
||||
@@ -181,9 +198,11 @@ class ColorSystem:
|
||||
for name, color in COLORS:
|
||||
is_dark_shade = dark and name in DARK_SHADES
|
||||
spread = luminosity_spread / 1.5 if is_dark_shade else luminosity_spread
|
||||
if name == "panel":
|
||||
spread /= 2
|
||||
for shade_name, luminosity_delta in luminosity_range(spread):
|
||||
if is_dark_shade:
|
||||
dark_background = background.blend(color, 12 / 100)
|
||||
dark_background = background.blend(color, 0.15)
|
||||
shade_color = dark_background.blend(
|
||||
WHITE, spread + luminosity_delta
|
||||
).clamped
|
||||
@@ -227,11 +246,13 @@ class ColorSystem:
|
||||
|
||||
if __name__ == "__main__":
|
||||
color_system = ColorSystem(
|
||||
primary="#005EA8",
|
||||
secondary="#F59402",
|
||||
warning="#ffa000",
|
||||
error="#C62828",
|
||||
success="#558B2F",
|
||||
primary="#406e8e", # blueish
|
||||
secondary="#6d9f71", # purpleish
|
||||
warning="#ffa62b", # orange
|
||||
error="#ba3c5b", # error
|
||||
success="#6d9f71", # green
|
||||
accent="#ffa62b",
|
||||
system="#5a4599",
|
||||
)
|
||||
|
||||
from rich import print
|
||||
|
||||
@@ -311,6 +311,9 @@ class MessagePump:
|
||||
async def post_priority_message(self, message: Message) -> bool:
|
||||
"""Post a "priority" messages which will be processes prior to regular messages.
|
||||
|
||||
Note that you should rarely need this in a regular app. It exists primarily to allow
|
||||
timer messages to skip the queue, so that they can be more regular.
|
||||
|
||||
Args:
|
||||
message (Message): A message.
|
||||
|
||||
@@ -344,7 +347,6 @@ class MessagePump:
|
||||
|
||||
async def on_callback(self, event: events.Callback) -> None:
|
||||
await invoke(event.callback)
|
||||
# await event.callback()
|
||||
|
||||
def emit_no_wait(self, message: Message) -> bool:
|
||||
if self._parent:
|
||||
|
||||
@@ -206,9 +206,18 @@ class ScrollBar(Widget):
|
||||
yield "position", self.position
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
styles = self.parent.styles
|
||||
style = Style(
|
||||
bgcolor=(Color.parse("#555555" if self.mouse_over else "#444444")),
|
||||
color=Color.parse("bright_yellow" if self.grabbed else "bright_magenta"),
|
||||
bgcolor=(
|
||||
styles.scrollbar_background_hover.rich_color
|
||||
if self.mouse_over
|
||||
else styles.scrollbar_background.rich_color
|
||||
),
|
||||
color=(
|
||||
styles.scrollbar_color_active.rich_color
|
||||
if self.grabbed
|
||||
else styles.scrollbar_color.rich_color
|
||||
),
|
||||
)
|
||||
return ScrollBarRender(
|
||||
virtual_size=self.window_virtual_size,
|
||||
|
||||
@@ -220,7 +220,7 @@ class Widget(DOMNode):
|
||||
y: float | None = None,
|
||||
*,
|
||||
animate: bool = True,
|
||||
):
|
||||
) -> bool:
|
||||
"""Scroll to a given (absolute) coordinate, optionally animating.
|
||||
|
||||
Args:
|
||||
@@ -229,61 +229,73 @@ class Widget(DOMNode):
|
||||
animate (bool, optional): Animate to new scroll position. Defaults to False.
|
||||
"""
|
||||
|
||||
scrolled_x = False
|
||||
scrolled_y = False
|
||||
|
||||
if animate:
|
||||
# TODO: configure animation speed
|
||||
if x is not None:
|
||||
self.scroll_target_x = x
|
||||
self.animate(
|
||||
"scroll_x", self.scroll_target_x, speed=80, easing="out_cubic"
|
||||
)
|
||||
if x != self.scroll_x:
|
||||
self.animate(
|
||||
"scroll_x", self.scroll_target_x, speed=80, easing="out_cubic"
|
||||
)
|
||||
scrolled_x = True
|
||||
if y is not None:
|
||||
self.scroll_target_y = y
|
||||
self.animate(
|
||||
"scroll_y", self.scroll_target_y, speed=80, easing="out_cubic"
|
||||
)
|
||||
if y != self.scroll_y:
|
||||
self.animate(
|
||||
"scroll_y", self.scroll_target_y, speed=80, easing="out_cubic"
|
||||
)
|
||||
scrolled_y = True
|
||||
|
||||
else:
|
||||
if x is not None:
|
||||
self.scroll_target_x = self.scroll_x = x
|
||||
if x != self.scroll_x:
|
||||
scrolled_x = True
|
||||
if y is not None:
|
||||
self.scroll_target_y = self.scroll_y = y
|
||||
self.refresh(layout=True)
|
||||
if y != self.scroll_y:
|
||||
scrolled_y = True
|
||||
self.refresh(repaint=False, layout=True)
|
||||
return scrolled_x or scrolled_y
|
||||
|
||||
def scroll_home(self, animate: bool = True) -> None:
|
||||
self.scroll_to(0, 0, animate=animate)
|
||||
def scroll_home(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(0, 0, animate=animate)
|
||||
|
||||
def scroll_end(self, animate: bool = True) -> None:
|
||||
self.scroll_to(0, self.max_scroll_y, animate=animate)
|
||||
def scroll_end(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(0, self.max_scroll_y, animate=animate)
|
||||
|
||||
def scroll_left(self, animate: bool = True) -> None:
|
||||
self.scroll_to(x=self.scroll_target_x - 1.5, animate=animate)
|
||||
def scroll_left(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(x=self.scroll_target_x - 1, animate=animate)
|
||||
|
||||
def scroll_right(self, animate: bool = True) -> None:
|
||||
self.scroll_to(x=self.scroll_target_x + 1.5, animate=animate)
|
||||
def scroll_right(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(x=self.scroll_target_x + 1, animate=animate)
|
||||
|
||||
def scroll_up(self, animate: bool = True) -> None:
|
||||
self.scroll_to(y=self.scroll_target_y + 1.5, animate=animate)
|
||||
def scroll_up(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(y=self.scroll_target_y + 1, animate=animate)
|
||||
|
||||
def scroll_down(self, animate: bool = True) -> None:
|
||||
self.scroll_to(y=self.scroll_target_y - 1.5, animate=animate)
|
||||
def scroll_down(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(y=self.scroll_target_y - 1, animate=animate)
|
||||
|
||||
def scroll_page_up(self, animate: bool = True) -> None:
|
||||
self.scroll_to(
|
||||
def scroll_page_up(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(
|
||||
y=self.scroll_target_y - self.container_size.height, animate=animate
|
||||
)
|
||||
|
||||
def scroll_page_down(self, animate: bool = True) -> None:
|
||||
self.scroll_to(
|
||||
def scroll_page_down(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(
|
||||
y=self.scroll_target_y + self.container_size.height, animate=animate
|
||||
)
|
||||
|
||||
def scroll_page_left(self, animate: bool = True) -> None:
|
||||
self.scroll_to(
|
||||
def scroll_page_left(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(
|
||||
x=self.scroll_target_x - self.container_size.width, animate=animate
|
||||
)
|
||||
|
||||
def scroll_page_right(self, animate: bool = True) -> None:
|
||||
self.scroll_to(
|
||||
def scroll_page_right(self, animate: bool = True) -> bool:
|
||||
return self.scroll_to(
|
||||
x=self.scroll_target_x + self.container_size.width, animate=animate
|
||||
)
|
||||
|
||||
@@ -525,6 +537,9 @@ class Widget(DOMNode):
|
||||
"""
|
||||
if self._dirty_regions:
|
||||
self._render_lines()
|
||||
if self.is_container:
|
||||
self.horizontal_scrollbar.refresh()
|
||||
self.vertical_scrollbar.refresh()
|
||||
lines = self._render_cache.lines[start:end]
|
||||
return lines
|
||||
|
||||
@@ -639,30 +654,40 @@ class Widget(DOMNode):
|
||||
def on_enter(self) -> None:
|
||||
self.mouse_over = True
|
||||
|
||||
def on_mouse_scroll_down(self) -> None:
|
||||
self.scroll_down(animate=False)
|
||||
def on_mouse_scroll_down(self, event) -> None:
|
||||
if self.is_container:
|
||||
if not self.scroll_down(animate=False):
|
||||
event.stop()
|
||||
|
||||
def on_mouse_scroll_up(self) -> None:
|
||||
self.scroll_up(animate=False)
|
||||
def on_mouse_scroll_up(self, event) -> None:
|
||||
if self.is_container:
|
||||
if not self.scroll_up(animate=False):
|
||||
event.stop()
|
||||
|
||||
def handle_scroll_to(self, message: ScrollTo) -> None:
|
||||
self.scroll_to(message.x, message.y, animate=message.animate)
|
||||
if self.is_container:
|
||||
self.scroll_to(message.x, message.y, animate=message.animate)
|
||||
message.stop()
|
||||
|
||||
def handle_scroll_up(self, event: ScrollUp) -> None:
|
||||
self.scroll_page_up()
|
||||
event.stop()
|
||||
if self.is_container:
|
||||
self.scroll_page_up()
|
||||
event.stop()
|
||||
|
||||
def handle_scroll_down(self, event: ScrollDown) -> None:
|
||||
self.scroll_page_down()
|
||||
event.stop()
|
||||
if self.is_container:
|
||||
self.scroll_page_down()
|
||||
event.stop()
|
||||
|
||||
def handle_scroll_left(self, event: ScrollLeft) -> None:
|
||||
self.scroll_page_left()
|
||||
event.stop()
|
||||
if self.is_container:
|
||||
self.scroll_page_left()
|
||||
event.stop()
|
||||
|
||||
def handle_scroll_right(self, event: ScrollRight) -> None:
|
||||
self.scroll_page_right()
|
||||
event.stop()
|
||||
if self.is_container:
|
||||
self.scroll_page_right()
|
||||
event.stop()
|
||||
|
||||
def key_home(self) -> bool:
|
||||
if self.is_container:
|
||||
|
||||
Reference in New Issue
Block a user