mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
auto sizing
This commit is contained in:
@@ -27,6 +27,7 @@ Widget {
|
||||
/* outline: heavy blue; */
|
||||
height: 10;
|
||||
padding: 1 2;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
max-height: 100vh;
|
||||
@@ -41,5 +42,7 @@ Widget {
|
||||
height: 10;
|
||||
margin: 1;
|
||||
background:blue;
|
||||
color: white 50%;
|
||||
border: white;
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
16
sandbox/auto_test.css
Normal file
16
sandbox/auto_test.css
Normal file
@@ -0,0 +1,16 @@
|
||||
Vertical {
|
||||
background: red 50%;
|
||||
}
|
||||
|
||||
.test {
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
background: white 50%;
|
||||
border:solid green;
|
||||
padding: 0;
|
||||
margin:3;
|
||||
|
||||
align: center middle;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
20
sandbox/auto_test.py
Normal file
20
sandbox/auto_test.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Static
|
||||
from textual.layout import Vertical
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
TEXT = Text.from_markup(" ".join(str(n) * 5 for n in range(10)))
|
||||
|
||||
|
||||
class AutoApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Vertical(
|
||||
Static(TEXT, classes="test"), Static(TEXT, id="test", classes="test")
|
||||
)
|
||||
|
||||
|
||||
app = AutoApp(css_path="auto_test.css")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -75,7 +75,7 @@ App > Screen {
|
||||
|
||||
|
||||
Tweet {
|
||||
height: 12;
|
||||
height: auto;
|
||||
width: 80;
|
||||
|
||||
margin: 1 3;
|
||||
@@ -85,7 +85,7 @@ Tweet {
|
||||
/* border: outer $primary; */
|
||||
padding: 1;
|
||||
border: wide $panel-darken-2;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
align-horizontal: center;
|
||||
|
||||
}
|
||||
@@ -223,4 +223,4 @@ Success {
|
||||
|
||||
.horizontal {
|
||||
layout: horizontal
|
||||
}
|
||||
}
|
||||
18
sandbox/horizontal.css
Normal file
18
sandbox/horizontal.css
Normal file
@@ -0,0 +1,18 @@
|
||||
Horizontal {
|
||||
background: red 50%;
|
||||
overflow-x: auto;
|
||||
width: auto
|
||||
}
|
||||
|
||||
.test {
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
background: white 50%;
|
||||
border:solid green;
|
||||
padding: 0;
|
||||
margin:3;
|
||||
|
||||
align: center middle;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
26
sandbox/horizontal.py
Normal file
26
sandbox/horizontal.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Static
|
||||
from textual import layout
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
TEXT = Text.from_markup(" ".join(str(n) * 5 for n in range(10)))
|
||||
|
||||
|
||||
class AutoApp(App):
|
||||
def on_mount(self) -> None:
|
||||
self.bind("t", "tree")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield layout.Horizontal(
|
||||
Static(TEXT, classes="test"), Static(TEXT, id="test", classes="test")
|
||||
)
|
||||
|
||||
def action_tree(self):
|
||||
self.log(self.screen.tree)
|
||||
|
||||
|
||||
app = AutoApp(css_path="horizontal.css")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -1,13 +1,14 @@
|
||||
App.-show-focus *:focus {
|
||||
tint: #8bc34a 50%;
|
||||
tint: #8bc34a 20%;
|
||||
}
|
||||
|
||||
#uber1 {
|
||||
layout: vertical;
|
||||
background: green;
|
||||
overflow: hidden auto;
|
||||
border: heavy white;
|
||||
border: heavy white;
|
||||
text-style: underline;
|
||||
/* box-sizing: content-box; */
|
||||
}
|
||||
|
||||
#uber1:focus-within {
|
||||
@@ -16,11 +17,12 @@ App.-show-focus *:focus {
|
||||
|
||||
#child2 {
|
||||
text-style: underline;
|
||||
background: red;
|
||||
background: red 10%;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
height: 20;
|
||||
height: 10;
|
||||
/* display: none; */
|
||||
color: #12a0;
|
||||
background: #ffffff00;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from functools import lru_cache
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
import rich.repr
|
||||
from rich.segment import Segment, SegmentLines
|
||||
from rich.style import Style, StyleType
|
||||
from rich.style import Style
|
||||
|
||||
from .color import Color
|
||||
from .css.types import EdgeStyle, EdgeType
|
||||
@@ -210,8 +210,8 @@ class Border:
|
||||
if new_height >= 1:
|
||||
render_options = options.update_dimensions(width, new_height)
|
||||
else:
|
||||
render_options = options
|
||||
has_top = has_bottom = False
|
||||
render_options = options.update_width(width)
|
||||
# has_top = has_bottom = False
|
||||
|
||||
lines = console.render_lines(self.renderable, render_options)
|
||||
|
||||
|
||||
@@ -41,3 +41,27 @@ class Layout(ABC):
|
||||
Returns:
|
||||
Iterable[WidgetPlacement]: An iterable of widget location
|
||||
"""
|
||||
|
||||
def get_content_width(
|
||||
self, parent: Widget, container_size: Size, viewport_size: Size
|
||||
) -> int:
|
||||
width: int | None = None
|
||||
for child in parent.displayed_children:
|
||||
if not child.is_container:
|
||||
child_width = child.get_content_width(container_size, viewport_size)
|
||||
width = child_width if width is None else max(width, child_width)
|
||||
if width is None:
|
||||
width = container_size.width
|
||||
return width
|
||||
|
||||
def get_content_height(
|
||||
self, parent: Widget, container_size: Size, viewport_size: Size, width: int
|
||||
) -> int:
|
||||
if not parent.displayed_children:
|
||||
height = container_size.height
|
||||
else:
|
||||
placements, widgets = self.arrange(
|
||||
parent, Size(width, container_size.height), Offset(0, 0)
|
||||
)
|
||||
height = max(placement.region.y_max for placement in placements)
|
||||
return height
|
||||
|
||||
@@ -415,7 +415,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
output = " ".join(str(arg) for arg in objects)
|
||||
if kwargs:
|
||||
key_values = " ".join(
|
||||
f"{key}={value}" for key, value in kwargs.items()
|
||||
f"{key}={value!r}" for key, value in kwargs.items()
|
||||
)
|
||||
output = f"{output} {key_values}" if output else key_values
|
||||
if self._log_console is not None:
|
||||
@@ -425,7 +425,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
DevtoolsLog(output, caller=_textual_calling_frame)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
self.console.bell()
|
||||
|
||||
def bind(
|
||||
self,
|
||||
@@ -470,14 +470,17 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
try:
|
||||
time = perf_counter()
|
||||
self.stylesheet.read(self.css_path)
|
||||
stylesheet = self.stylesheet.copy()
|
||||
stylesheet.read(self.css_path)
|
||||
stylesheet.parse()
|
||||
elapsed = (perf_counter() - time) * 1000
|
||||
self.log(f"loaded {self.css_path} in {elapsed:.0f}ms")
|
||||
self.log(f"<stylesheet> loaded {self.css_path!r} in {elapsed:.0f} ms")
|
||||
except Exception as error:
|
||||
# TODO: Catch specific exceptions
|
||||
self.console.bell()
|
||||
self.log(error)
|
||||
else:
|
||||
self.stylesheet = stylesheet
|
||||
self.reset_styles()
|
||||
self.stylesheet.update(self)
|
||||
self.screen.refresh(layout=True)
|
||||
|
||||
@@ -17,8 +17,8 @@ def get_box_model(
|
||||
styles: StylesBase,
|
||||
container: Size,
|
||||
viewport: Size,
|
||||
get_content_width: Callable[[Size, Size], int],
|
||||
get_content_height: Callable[[Size, Size, int], int],
|
||||
get_content_width: Callable[[Size, Size], int | None],
|
||||
get_content_height: Callable[[Size, Size, int], int | None],
|
||||
) -> BoxModel:
|
||||
"""Resolve the box model for this Styles.
|
||||
|
||||
@@ -36,34 +36,26 @@ def get_box_model(
|
||||
has_rule = styles.has_rule
|
||||
width, height = container
|
||||
is_content_box = styles.box_sizing == "content-box"
|
||||
is_border_box = styles.box_sizing == "border-box"
|
||||
gutter = styles.padding + styles.border.spacing
|
||||
|
||||
is_auto_width = styles.width and styles.width.is_auto
|
||||
is_auto_height = styles.height and styles.height.is_auto
|
||||
|
||||
if not has_rule("width"):
|
||||
width = container.width
|
||||
elif styles.width.is_auto:
|
||||
elif is_auto_width:
|
||||
# When width is auto, we want enough space to always fit the content
|
||||
width = get_content_width(container, viewport)
|
||||
if not is_content_box:
|
||||
# If box sizing is border box we want to enlarge the width so that it
|
||||
# can accommodate padding + border
|
||||
width += gutter.width
|
||||
width = get_content_width(
|
||||
(container - gutter.totals if is_border_box else container)
|
||||
- styles.margin.totals,
|
||||
viewport,
|
||||
)
|
||||
# width = min(container.width, width)
|
||||
|
||||
else:
|
||||
width = styles.width.resolve_dimension(container, viewport)
|
||||
|
||||
if not has_rule("height"):
|
||||
height = container.height
|
||||
elif styles.height.is_auto:
|
||||
height = get_content_height(container, viewport, width)
|
||||
if not is_content_box:
|
||||
height += gutter.height
|
||||
else:
|
||||
height = styles.height.resolve_dimension(container, viewport)
|
||||
|
||||
if is_content_box:
|
||||
gutter_width, gutter_height = gutter.totals
|
||||
width += gutter_width
|
||||
height += gutter_height
|
||||
|
||||
if has_rule("min_width"):
|
||||
min_width = styles.min_width.resolve_dimension(container, viewport)
|
||||
width = max(width, min_width)
|
||||
@@ -72,6 +64,17 @@ def get_box_model(
|
||||
max_width = styles.max_width.resolve_dimension(container, viewport)
|
||||
width = min(width, max_width)
|
||||
|
||||
if not has_rule("height"):
|
||||
height = container.height
|
||||
elif styles.height.is_auto:
|
||||
height = get_content_height(
|
||||
container - gutter.totals if is_border_box else container, viewport, width
|
||||
)
|
||||
if is_border_box:
|
||||
height += gutter.height
|
||||
else:
|
||||
height = styles.height.resolve_dimension(container, viewport)
|
||||
|
||||
if has_rule("min_height"):
|
||||
min_height = styles.min_height.resolve_dimension(container, viewport)
|
||||
height = max(height, min_height)
|
||||
@@ -80,7 +83,16 @@ def get_box_model(
|
||||
max_height = styles.max_height.resolve_dimension(container, viewport)
|
||||
height = min(height, max_height)
|
||||
|
||||
if is_border_box and is_auto_width:
|
||||
width += gutter.width
|
||||
|
||||
if is_content_box:
|
||||
width += gutter.width
|
||||
height += gutter.height
|
||||
|
||||
size = Size(width, height)
|
||||
margin = styles.margin
|
||||
|
||||
return BoxModel(size, margin)
|
||||
model = BoxModel(size, margin)
|
||||
|
||||
return model
|
||||
|
||||
@@ -82,7 +82,9 @@ class Scalar(NamedTuple):
|
||||
percent_unit: Unit
|
||||
|
||||
def __str__(self) -> str:
|
||||
value, _unit, _ = self
|
||||
value, unit, _ = self
|
||||
if unit == Unit.AUTO:
|
||||
return "auto"
|
||||
return f"{int(value) if value.is_integer() else value}{self.symbol}"
|
||||
|
||||
@property
|
||||
|
||||
@@ -141,6 +141,11 @@ class Stylesheet:
|
||||
def css(self) -> str:
|
||||
return "\n\n".join(rule_set.css for rule_set in self.rules)
|
||||
|
||||
def copy(self) -> Stylesheet:
|
||||
stylesheet = Stylesheet(variables=self.variables.copy())
|
||||
stylesheet.source = self.source.copy()
|
||||
return stylesheet
|
||||
|
||||
def set_variables(self, variables: dict[str, str]) -> None:
|
||||
"""Set CSS variables.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import datetime
|
||||
from time import time
|
||||
import inspect
|
||||
import json
|
||||
import pickle
|
||||
@@ -207,7 +207,7 @@ class DevtoolsClient:
|
||||
{
|
||||
"type": "client_log",
|
||||
"payload": {
|
||||
"timestamp": int(datetime.datetime.utcnow().timestamp()),
|
||||
"timestamp": int(time()),
|
||||
"path": getattr(log.caller, "filename", ""),
|
||||
"line_number": getattr(log.caller, "lineno", 0),
|
||||
"encoded_segments": encoded_segments,
|
||||
@@ -242,6 +242,6 @@ class DevtoolsClient:
|
||||
Returns:
|
||||
str: The Segment list pickled with pickle protocol v3, then base64 encoded
|
||||
"""
|
||||
pickled = pickle.dumps(segments, protocol=3)
|
||||
pickled = pickle.dumps(segments, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
encoded = base64.b64encode(pickled)
|
||||
return str(encoded, encoding="utf-8")
|
||||
|
||||
@@ -72,19 +72,13 @@ class DevConsoleLog:
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
local_time = (
|
||||
datetime.fromtimestamp(self.unix_timestamp)
|
||||
.replace(tzinfo=timezone.utc)
|
||||
.astimezone(tz=datetime.now().astimezone().tzinfo)
|
||||
)
|
||||
timezone_name = local_time.tzname()
|
||||
local_time = datetime.fromtimestamp(self.unix_timestamp)
|
||||
|
||||
table = Table.grid(expand=True)
|
||||
table.add_column()
|
||||
table.add_column()
|
||||
file_link = escape(f"file://{Path(self.path).absolute()}")
|
||||
file_and_line = escape(f"{Path(self.path).name}:{self.line_number}")
|
||||
table.add_row(
|
||||
f"[dim]{local_time.time()} {timezone_name}",
|
||||
f"[dim]{local_time.time()}",
|
||||
Align.right(
|
||||
Text(f"{file_and_line}", style=Style(dim=True, link=file_link))
|
||||
),
|
||||
|
||||
@@ -179,7 +179,7 @@ class ClientHandler:
|
||||
and message_time - last_message_time > 1
|
||||
):
|
||||
# Print a rule if it has been longer than a second since the last message
|
||||
self.service.console.rule("")
|
||||
self.service.console.rule()
|
||||
self.service.console.print(
|
||||
DevConsoleLog(
|
||||
segments=segments,
|
||||
|
||||
@@ -270,7 +270,7 @@ class DOMNode(MessagePump):
|
||||
return tree
|
||||
|
||||
@property
|
||||
def rich_text_style(self) -> Style:
|
||||
def text_style(self) -> Style:
|
||||
"""Get the text style object.
|
||||
|
||||
A widget's style is influenced by its parent. For instance if a widgets background has an alpha,
|
||||
@@ -283,19 +283,25 @@ class DOMNode(MessagePump):
|
||||
|
||||
# TODO: Feels like there may be opportunity for caching here.
|
||||
|
||||
background = Color(0, 0, 0, 0)
|
||||
color = Color(255, 255, 255, 0)
|
||||
style = Style()
|
||||
for node in reversed(self.ancestors):
|
||||
style += node.styles.text_style
|
||||
|
||||
return style
|
||||
|
||||
@property
|
||||
def colors(self) -> tuple[tuple[Color, Color], tuple[Color, Color]]:
|
||||
base_background = background = Color(0, 0, 0, 0)
|
||||
base_color = color = Color(255, 255, 255, 0)
|
||||
for node in reversed(self.ancestors):
|
||||
styles = node.styles
|
||||
if styles.has_rule("background"):
|
||||
base_background = background
|
||||
background += styles.background
|
||||
if styles.has_rule("color"):
|
||||
color = styles.color
|
||||
style += styles.text_style
|
||||
|
||||
style = Style(bgcolor=background.rich_color, color=color.rich_color) + style
|
||||
return style
|
||||
base_color = color
|
||||
color += styles.color
|
||||
return (base_background, base_color), (background, color)
|
||||
|
||||
@property
|
||||
def ancestors(self) -> list[DOMNode]:
|
||||
|
||||
@@ -50,8 +50,6 @@ class HorizontalLayout(Layout):
|
||||
x += region.width + margin
|
||||
max_width = x
|
||||
|
||||
max_width += margins[-1] if margins else 0
|
||||
|
||||
total_region = Region(0, 0, max_width, max_height)
|
||||
add_placement(WidgetPlacement(total_region, None, 0))
|
||||
|
||||
|
||||
@@ -47,9 +47,17 @@ class VerticalLayout(Layout):
|
||||
y += region.height + margin
|
||||
max_height = y
|
||||
|
||||
max_height += margins[-1] if margins else 0
|
||||
# max_height += margins[-1] if margins else 0
|
||||
|
||||
total_region = Region(0, 0, max_width, max_height)
|
||||
add_placement(WidgetPlacement(total_region, None, 0))
|
||||
|
||||
return placements, set(displayed_children)
|
||||
|
||||
# def get_content_width(
|
||||
# self, parent: Widget, container_size: Size, viewport_size: Size
|
||||
# ) -> int:
|
||||
# width = super().get_content_width(parent, container_size, viewport_size)
|
||||
# width = min(width, container_size.width)
|
||||
# print("get_content_width", parent, container_size, width)
|
||||
# return width
|
||||
|
||||
@@ -70,6 +70,7 @@ class Widget(DOMNode):
|
||||
can_focus_children: bool = True
|
||||
|
||||
CSS = """
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -111,6 +112,14 @@ class Widget(DOMNode):
|
||||
show_vertical_scrollbar = Reactive(False, layout=True)
|
||||
show_horizontal_scrollbar = Reactive(False, layout=True)
|
||||
|
||||
def watch_show_horizontal_scrollbar(self, value: bool) -> None:
|
||||
if not value:
|
||||
self.scroll_to(0, 0, animate=False)
|
||||
|
||||
def watch_show_vertical_scrollbar(self, value: bool) -> None:
|
||||
if not value:
|
||||
self.scroll_to(0, 0, animate=False)
|
||||
|
||||
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
||||
self.app.register(self, *anon_widgets, **widgets)
|
||||
self.screen.refresh()
|
||||
@@ -160,10 +169,15 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
int: The optimal width of the content.
|
||||
"""
|
||||
if self.is_container:
|
||||
return self.layout.get_content_width(self, container_size, viewport_size)
|
||||
console = self.app.console
|
||||
renderable = self.render(self.styles.rich_style)
|
||||
measurement = Measurement.get(console, console.options, renderable)
|
||||
return measurement.maximum
|
||||
measurement = Measurement.get(
|
||||
console, console.options.update_width(container_size.width), renderable
|
||||
)
|
||||
width = measurement.maximum
|
||||
return width
|
||||
|
||||
def get_content_height(
|
||||
self, container_size: Size, viewport_size: Size, width: int
|
||||
@@ -178,11 +192,20 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
int: The height of the content.
|
||||
"""
|
||||
renderable = self.render(self.styles.rich_style)
|
||||
options = self.console.options.update_width(width)
|
||||
segments = self.console.render(renderable, options)
|
||||
# Cheaper than counting the lines returned from render_lines!
|
||||
height = sum(text.count("\n") for text, _, _ in segments)
|
||||
|
||||
if self.is_container:
|
||||
assert self.layout is not None
|
||||
return self.layout.get_content_height(
|
||||
self, container_size, viewport_size, width
|
||||
)
|
||||
else:
|
||||
renderable = self.render(self.styles.rich_style)
|
||||
options = self.console.options.update_width(width).update(highlight=False)
|
||||
|
||||
segments = self.console.render(renderable, options)
|
||||
# # Cheaper than counting the lines returned from render_lines!
|
||||
# print(list(segments))
|
||||
height = sum(text.count("\n") for text, _, _ in segments)
|
||||
return height
|
||||
|
||||
async def watch_scroll_x(self, new_value: float) -> None:
|
||||
@@ -556,32 +579,28 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
RenderableType: A new renderable.
|
||||
"""
|
||||
renderable = self.render(self.styles.rich_style)
|
||||
renderable = self.render(self.text_style)
|
||||
|
||||
(base_background, base_color), (background, color) = self.colors
|
||||
styles = self.styles
|
||||
parent_styles = self.parent.styles
|
||||
|
||||
parent_text_style = self.parent.rich_text_style
|
||||
text_style = styles.rich_style
|
||||
|
||||
content_align = (styles.content_align_horizontal, styles.content_align_vertical)
|
||||
if content_align != ("left", "top"):
|
||||
horizontal, vertical = content_align
|
||||
renderable = Align(renderable, horizontal, vertical=vertical)
|
||||
|
||||
renderable = Padding(renderable, styles.padding)
|
||||
|
||||
renderable_text_style = parent_text_style + text_style
|
||||
if renderable_text_style:
|
||||
style = Style.from_color(text_style.color, text_style.bgcolor)
|
||||
renderable = Styled(renderable, style)
|
||||
renderable = Padding(
|
||||
renderable,
|
||||
styles.padding,
|
||||
style=Style.from_color(color.rich_color, background.rich_color),
|
||||
)
|
||||
|
||||
if styles.border:
|
||||
renderable = Border(
|
||||
renderable,
|
||||
styles.border,
|
||||
inner_color=styles.background,
|
||||
outer_color=Color.from_rich_color(parent_text_style.bgcolor),
|
||||
inner_color=background,
|
||||
outer_color=base_background,
|
||||
)
|
||||
|
||||
if styles.outline:
|
||||
@@ -589,7 +608,7 @@ class Widget(DOMNode):
|
||||
renderable,
|
||||
styles.outline,
|
||||
inner_color=styles.background,
|
||||
outer_color=parent_styles.background,
|
||||
outer_color=base_background,
|
||||
outline=True,
|
||||
)
|
||||
|
||||
@@ -706,6 +725,8 @@ class Widget(DOMNode):
|
||||
self.horizontal_scrollbar.window_virtual_size = virtual_size.width
|
||||
self.horizontal_scrollbar.window_size = width
|
||||
|
||||
self.scroll_x = self.validate_scroll_x(self.scroll_x)
|
||||
self.scroll_y = self.validate_scroll_y(self.scroll_y)
|
||||
self.refresh(layout=True)
|
||||
self.call_later(self.scroll_to, self.scroll_x, self.scroll_y)
|
||||
else:
|
||||
@@ -865,10 +886,12 @@ class Widget(DOMNode):
|
||||
def on_focus(self, event: events.Focus) -> None:
|
||||
self.emit_no_wait(events.DescendantFocus(self))
|
||||
self.has_focus = True
|
||||
self.refresh()
|
||||
|
||||
def on_blur(self, event: events.Blur) -> None:
|
||||
self.emit_no_wait(events.DescendantBlur(self))
|
||||
self.has_focus = False
|
||||
self.refresh()
|
||||
|
||||
def on_descendant_focus(self, event: events.DescendantFocus) -> None:
|
||||
self.descendant_has_focus = True
|
||||
@@ -880,13 +903,13 @@ class Widget(DOMNode):
|
||||
|
||||
def on_mouse_scroll_down(self, event) -> None:
|
||||
if self.is_container:
|
||||
self.scroll_down(animate=False)
|
||||
event.stop()
|
||||
if self.scroll_down(animate=False):
|
||||
event.stop()
|
||||
|
||||
def on_mouse_scroll_up(self, event) -> None:
|
||||
if self.is_container:
|
||||
self.scroll_up(animate=False)
|
||||
event.stop()
|
||||
if self.scroll_up(animate=False):
|
||||
event.stop()
|
||||
|
||||
def handle_scroll_to(self, message: ScrollTo) -> None:
|
||||
if self.is_container:
|
||||
|
||||
Reference in New Issue
Block a user