Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `Markdown.get_stream` https://github.com/Textualize/textual/pull/5966
- Added `textual.highlight` module for syntax highlighting https://github.com/Textualize/textual/pull/5966
- Added `MessagePump.wait_for_refresh` method https://github.com/Textualize/textual/pull/5966
- Added `Widget.container_scroll_offset` https://github.com/Textualize/textual/commit/e84600cfb31630f8b5493bf1043a4a1e7c212f7c
- Added `Markdown.source` attribute to MarkdownBlocks https://github.com/Textualize/textual/commit/e84600cfb31630f8b5493bf1043a4a1e7c212f7c
- Added extension mechanism to Markdown https://github.com/Textualize/textual/commit/e84600cfb31630f8b5493bf1043a4a1e7c212f7c
- Added `index` to `ListView.Selected` event https://github.com/Textualize/textual/pull/5973
- Added `layout` switch to Static.update https://github.com/Textualize/textual/pull/5973

### Changed

- Improved rendering of Markdown tables (replace Rich table with grid) which allows text selection https://github.com/Textualize/textual/pull/5962
- Change look of command palette, to drop accented borders https://github.com/Textualize/textual/pull/5966
- Some style tweaks to Markdown https://github.com/Textualize/textual/commit/e84600cfb31630f8b5493bf1043a4a1e7c212f7c


### Removed

- Breaking change: Removed `Markdown.code_dark_theme`, `Markdown.code_light_theme`, `Markdown.code_indent_guides` which are no longer needed with the new code fence. https://github.com/Textualize/textual/pull/5967
- Removed focus style from Markdown, as it can be a little expensive https://github.com/Textualize/textual/commit/e84600cfb31630f8b5493bf1043a4a1e7c212f7c

## [4.0.0] - 2025-07-12

Expand Down
2 changes: 0 additions & 2 deletions src/textual/_arrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,8 @@ def arrange(
)

WidgetPlacement.apply_absolute(layout_placements)

placements.extend(layout_placements)

widget.log(placements)
return DockArrangeResult(placements, set(display_widgets), scroll_spacing)


Expand Down
2 changes: 1 addition & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1375,7 +1375,7 @@ def _watch_theme(self, theme_name: str) -> None:
self.set_class(not dark, "-light-mode", update=False)
self._refresh_truecolor_filter(self.ansi_theme)
self._invalidate_css()
self.call_next(self.refresh_css)
self.call_next(partial(self.refresh_css, animate=False))
self.call_next(self.theme_changed_signal.publish, theme)

def _invalidate_css(self) -> None:
Expand Down
9 changes: 7 additions & 2 deletions src/textual/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,9 @@ def styled(
if not text:
return Content("")
span_length = cell_len(text) if cell_length is None else cell_length
new_content = cls(text, [Span(0, span_length, style)], span_length)
new_content = cls(
text, [Span(0, span_length, style)] if style else None, span_length
)
return new_content

@classmethod
Expand Down Expand Up @@ -822,6 +824,7 @@ def iter_content() -> Iterable[Content]:
extend_spans(
_Span(offset + start, offset + end, style)
for start, end, style in content._spans
if style
)
offset += len(content._text)
if total_cell_length is not None:
Expand Down Expand Up @@ -1122,7 +1125,7 @@ def render(
get_style: Callable[[str | Style], Style]
if parse_style is None:

def get_style(style: str | Style) -> Style:
def _get_style(style: str | Style) -> Style:
"""The default get_style method."""
if isinstance(style, Style):
return style
Expand All @@ -1132,6 +1135,8 @@ def get_style(style: str | Style) -> Style:
visual_style = Style.null()
return visual_style

get_style = _get_style

else:
get_style = parse_style

Expand Down
18 changes: 18 additions & 0 deletions src/textual/css/_help_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
VALID_ALIGN_HORIZONTAL,
VALID_ALIGN_VERTICAL,
VALID_BORDER,
VALID_EXPAND,
VALID_KEYLINE,
VALID_LAYOUT,
VALID_POSITION,
Expand Down Expand Up @@ -788,6 +789,23 @@ def position_help_text(property_name: str) -> HelpText:
)


def expand_help_text(property_name: str) -> HelpText:
"""Help text to show when the user supplies the wrong value for expand.

Args:
property_name: The name of the property.

Returns:
Renderable for displaying the help text for this property.
"""
return HelpText(
summary=f"Invalid value for [i]{property_name}[/]",
bullets=[
Bullet(f"Valid values are {friendly_list(VALID_EXPAND)}"),
],
)


def style_flags_property_help_text(
property_name: str, value: str, context: StylingContext
) -> HelpText:
Expand Down
13 changes: 13 additions & 0 deletions src/textual/css/_styles_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
border_property_help_text,
color_property_help_text,
dock_property_help_text,
expand_help_text,
fractional_property_help_text,
integer_help_text,
keyline_help_text,
Expand Down Expand Up @@ -44,6 +45,7 @@
VALID_CONSTRAIN,
VALID_DISPLAY,
VALID_EDGE,
VALID_EXPAND,
VALID_HATCH,
VALID_KEYLINE,
VALID_OVERFLOW,
Expand Down Expand Up @@ -1256,6 +1258,17 @@ def process_hatch(self, name: str, tokens: list[Token]) -> None:

self.styles._rules[name] = (character or " ", color.multiply_alpha(opacity))

def process_expand(self, name: str, tokens: list[Token]):
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], offset_single_axis_help_text(name))
else:
token = tokens[0]
if token.value not in VALID_EXPAND:
self.error(name, tokens[0], expand_help_text(name))
self.styles._rules["expand"] = token.value

def _get_suggested_property_name_for_rule(self, rule_name: str) -> str | None:
"""
Returns a valid CSS property "Python" name, or None if no close matches could be found.
Expand Down
1 change: 1 addition & 0 deletions src/textual/css/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
VALID_HATCH: Final = {"left", "right", "cross", "vertical", "horizontal"}
VALID_TEXT_WRAP: Final = {"wrap", "nowrap"}
VALID_TEXT_OVERFLOW: Final = {"clip", "fold", "ellipsis"}
VALID_EXPAND: Final = {"greedy", "optimal"}

HATCHES: Final = {
"left": "╲",
Expand Down
2 changes: 0 additions & 2 deletions src/textual/css/scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,6 @@ def resolve(

if unit == Unit.PERCENT:
unit = percent_unit
# elif unit == Unit.AUTO:
# unit = Unit.FRACTION
try:
dimension = RESOLVE_MAP[unit](
value, size, viewport, fraction_unit or _FRACTION_ONE
Expand Down
6 changes: 6 additions & 0 deletions src/textual/css/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
VALID_BOX_SIZING,
VALID_CONSTRAIN,
VALID_DISPLAY,
VALID_EXPAND,
VALID_OVERFLOW,
VALID_OVERLAY,
VALID_POSITION,
Expand All @@ -61,6 +62,7 @@
BoxSizing,
Constrain,
Display,
Expand,
Overflow,
Overlay,
ScrollbarGutter,
Expand Down Expand Up @@ -203,6 +205,7 @@ class RulesMap(TypedDict, total=False):

text_wrap: TextWrap
text_overflow: TextOverflow
expand: Expand

line_pad: int

Expand Down Expand Up @@ -492,6 +495,7 @@ class StylesBase:
text_overflow: StringEnumProperty[TextOverflow] = StringEnumProperty(
VALID_TEXT_OVERFLOW, "fold"
)
expand: StringEnumProperty[Expand] = StringEnumProperty(VALID_EXPAND, "greedy")
line_pad = IntegerProperty(default=0, layout=True)
"""Padding added to left and right of lines."""

Expand Down Expand Up @@ -1288,6 +1292,8 @@ def append_declaration(name: str, value: str) -> None:
append_declaration("text-wrap", self.text_wrap)
if "text_overflow" in rules:
append_declaration("text-overflow", self.text_overflow)
if "expand" in rules:
append_declaration("expand", self.expand)
if "line_pad" in rules:
append_declaration("line-pad", str(self.line_pad))
lines.sort()
Expand Down
1 change: 0 additions & 1 deletion src/textual/css/stylesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,6 @@ def replace_rules(

for key in modified_rule_keys:
setattr(base_styles, key, get_rule(key))

node.notify_style_update()

def update(self, root: DOMNode, animate: bool = False) -> None:
Expand Down
1 change: 1 addition & 0 deletions src/textual/css/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
Position = Literal["relative", "absolute"]
TextWrap = Literal["wrap", "nowrap"]
TextOverflow = Literal["clip", "fold", "ellipsis"]
Expand = Literal["greedy", "expand"]

Specificity3 = Tuple[int, int, int]
Specificity6 = Tuple[int, int, int, int, int, int]
Expand Down
20 changes: 14 additions & 6 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,6 @@ class Widget(DOMNode):
"""Rich renderable may expand beyond optimal size."""
shrink: Reactive[bool] = Reactive(True)
"""Rich renderable may shrink below optimal size."""
greedy: Reactive[bool] = Reactive(True)
"""Fraction widths will consume as much space as possible."""
auto_links: Reactive[bool] = Reactive(True)
"""Widget will highlight links automatically."""
disabled: Reactive[bool] = Reactive(False)
Expand Down Expand Up @@ -1166,6 +1164,19 @@ def iter_styles() -> Iterable[StylesBase]:

return visual_style

def _get_style(self, style: str) -> VisualStyle | None:
"""A get_style method for use in Content.

Args:
style: A style prefixed with a dot.

Returns:
A visual style if one is fund, otherwise `None`.
"""
if style.startswith("."):
return self.get_visual_style(style[1:])
return None

@overload
def render_str(self, text_content: str) -> Content: ...

Expand Down Expand Up @@ -1558,9 +1569,6 @@ def _get_box_model(
The size and margin for this widget.
"""
styles = self.styles
# _content_width, _content_height = container
# content_width = Fraction(_content_width)
# content_height = Fraction(_content_height)
is_border_box = styles.box_sizing == "border-box"
gutter = styles.gutter # Padding plus border
margin = styles.margin
Expand Down Expand Up @@ -2198,7 +2206,7 @@ def _has_relative_children_width(self) -> bool:
if not self.is_container:
return False
for child in self.children:
if not child.greedy:
if child.styles.expand == "optimal":
continue
styles = child.styles
if styles.display == "none":
Expand Down
8 changes: 5 additions & 3 deletions src/textual/widgets/_list_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,14 @@ class Selected(Message):
ALLOW_SELECTOR_MATCH = {"item"}
"""Additional message attributes that can be used with the [`on` decorator][textual.on]."""

def __init__(self, list_view: ListView, item: ListItem) -> None:
def __init__(self, list_view: ListView, item: ListItem, index: int) -> None:
super().__init__()
self.list_view: ListView = list_view
"""The view that contains the item selected."""
self.item: ListItem = item
"""The selected item."""
self.index = index
"""Index of the selected item."""

@property
def control(self) -> ListView:
Expand Down Expand Up @@ -356,7 +358,7 @@ def action_select_cursor(self) -> None:
selected_child = self.highlighted_child
if selected_child is None:
return
self.post_message(self.Selected(self, selected_child))
self.post_message(self.Selected(self, selected_child, self.index))

def action_cursor_down(self) -> None:
"""Highlight the next item in the list."""
Expand Down Expand Up @@ -387,7 +389,7 @@ def _on_list_item__child_clicked(self, event: ListItem._ChildClicked) -> None:
event.stop()
self.focus()
self.index = self._nodes.index(event.item)
self.post_message(self.Selected(self, event.item))
self.post_message(self.Selected(self, event.item, self.index))

def __len__(self) -> int:
"""Compute the length (in number of items) of the list view."""
Expand Down
Loading
Loading