Merge pull request #6132 from Textualize/hover-fix

WIP hover fix
This commit is contained in:
Will McGugan
2025-09-26 20:14:03 +01:00
committed by GitHub
13 changed files with 156 additions and 48 deletions

View File

@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Eager tasks are now enabled On Python3.12 and above https://github.com/Textualize/textual/pull/6102
- `Widget._arrange` is now public (as `Widget.arrange`) https://github.com/Textualize/textual/pull/6108
- Reduced number of layout operations required to update the screen https://github.com/Textualize/textual/pull/6108
- The :hover pseudo-class no applies to the first widget under the mouse with a hover style set https://github.com/Textualize/textual/pull/6132
- The footer key hover background is more visible https://github.com/Textualize/textual/pull/6132
### Added
@@ -19,6 +21,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `Widget.process_layout` https://github.com/Textualize/textual/pull/6105
- Added `App.viewport_size` https://github.com/Textualize/textual/pull/6105
- Added `Screen.size` https://github.com/Textualize/textual/pull/6105
- Added `compact` to Binding.Group https://github.com/Textualize/textual/pull/6132
- Added `Screen.get_hover_widgets_at` https://github.com/Textualize/textual/pull/6132
### Fixed

View File

@@ -848,9 +848,10 @@ class Compositor:
Sequence of (WIDGET, REGION) tuples.
"""
contains = Region.contains
for widget, cropped_region, region in self.layers_visible[y]:
if contains(cropped_region, x, y) and widget.visible:
yield widget, region
if len(self.layers_visible) > y >= 0:
for widget, cropped_region, region in self.layers_visible[y]:
if contains(cropped_region, x, y) and widget.visible:
yield widget, region
def get_style_at(self, x: int, y: int) -> Style:
"""Get the Style at the given cell or Style.null()

View File

@@ -617,6 +617,9 @@ class App(Generic[ReturnType], DOMNode):
self._sync_available = False
self.mouse_over: Widget | None = None
"""The widget directly under the mouse."""
self.hover_over: Widget | None = None
"""The first widget with a hover style under the mouse."""
self.mouse_captured: Widget | None = None
self._driver: Driver | None = None
self._exit_renderables: list[RenderableType] = []
@@ -3010,7 +3013,9 @@ class App(Generic[ReturnType], DOMNode):
"""
self.screen.set_focus(widget, scroll_visible)
def _set_mouse_over(self, widget: Widget | None) -> None:
def _set_mouse_over(
self, widget: Widget | None, hover_widget: Widget | None
) -> None:
"""Called when the mouse is over another widget.
Args:
@@ -3031,6 +3036,12 @@ class App(Generic[ReturnType], DOMNode):
widget.post_message(events.Enter(widget))
finally:
self.mouse_over = widget
if self.hover_over is not None:
self.hover_over.mouse_hover = False
if hover_widget is not None:
hover_widget.mouse_hover = True
self.hover_over = hover_widget
def _update_mouse_over(self, screen: Screen) -> None:
"""Updates the mouse over after the next refresh.
@@ -3046,12 +3057,16 @@ class App(Generic[ReturnType], DOMNode):
async def check_mouse() -> None:
"""Check if the mouse over widget has changed."""
try:
widget, _ = screen.get_widget_at(*self.mouse_position)
hover_widgets = screen.get_hover_widgets_at(*self.mouse_position)
except NoWidget:
pass
else:
if widget is not self.mouse_over:
self._set_mouse_over(widget)
mouse_over, hover_over = hover_widgets.widgets
if (
mouse_over is not self.mouse_over
or hover_over is not self.hover_over
):
self._set_mouse_over(mouse_over, hover_over)
self.call_after_refresh(check_mouse)

View File

@@ -91,6 +91,9 @@ class Binding:
description: str = ""
"""Description of the group."""
compact: bool = False
"""Show keys in compact form (no spaces)."""
group: Group | None = None
"""Optional binding group (used to group related bindings in the footer)."""

View File

@@ -251,7 +251,7 @@ class ColorSystem:
"block-cursor-blurred-text-style", "none"
)
colors["block-hover-background"] = get(
"block-hover-background", boost.with_alpha(0.05).hex
"block-hover-background", boost.with_alpha(0.1).hex
)
# The border color for focused widgets which have a border.

View File

@@ -83,7 +83,6 @@ class HorizontalLayout(Layout):
children, box_models, margins
):
styles = widget.styles
overlay = styles.overlay == "screen"
offset = (
styles.offset.resolve(

View File

@@ -65,6 +65,12 @@ class StreamLayout(Layout):
if height < (max_height_value := int(max_height.value))
else max_height_value
)
if (min_height := styles.min_height) is not None and min_height.is_cells:
height = (
height
if height > (min_height_value := int(min_height.value))
else min_height_value
)
placements.append(
_WidgetPlacement(
_Region(left, y, width - (left + right), height),

View File

@@ -105,7 +105,6 @@ class VerticalLayout(Layout):
content_width.__floor__(),
next_y.__floor__() - y.__floor__(),
)
absolute = styles.has_rule("position") and styles.position == "absolute"
add_placement(
_WidgetPlacement(

View File

@@ -20,6 +20,7 @@ from typing import (
Generic,
Iterable,
Iterator,
NamedTuple,
Optional,
TypeVar,
Union,
@@ -83,6 +84,23 @@ ScreenResultCallbackType = Union[
"""Type of a screen result callback function."""
class HoverWidgets(NamedTuple):
"""Result of [get_hover_widget_at][textual.screen.Screen.get_hover_widget_at]"""
mouse_over: tuple[Widget, Region]
"""Widget and region directly under the mouse."""
hover_over: tuple[Widget, Region] | None
"""Widget with a hover style under the mouse, or `None` for no hover style widget."""
@property
def widgets(self) -> tuple[Widget, Widget | None]:
"""Just the widgets."""
return (
self.mouse_over[0],
None if self.hover_over is None else self.hover_over[0],
)
@rich.repr.auto
class ResultCallback(Generic[ScreenResultType]):
"""Holds the details of a callback."""
@@ -584,6 +602,33 @@ class Screen(Generic[ScreenResultType], Widget):
"""
return self._compositor.get_widget_at(x, y)
def get_hover_widgets_at(self, x: int, y: int) -> HoverWidgets:
"""Get the widget, and its region directly under the mouse, and the first
widget, region pair with a hover style.
Args:
x: X Coordinate.
y: Y Coordinate.
Returns:
A pair of (WIDGET, REGION) tuples for the top most and first hover style respectively.
Raises:
NoWidget: If there is no widget under the screen coordinate.
"""
widgets_under_coordinate = iter(self._compositor.get_widgets_at(x, y))
try:
top_widget, top_region = next(widgets_under_coordinate)
except StopIteration:
raise errors.NoWidget(f"No hover widget under screen coordinate ({x}, {y})")
if not top_widget._has_hover_style:
for widget, region in widgets_under_coordinate:
if widget._has_hover_style:
return HoverWidgets((top_widget, top_region), (widget, region))
return HoverWidgets((top_widget, top_region), None)
return HoverWidgets((top_widget, top_region), (top_widget, top_region))
def get_widgets_at(self, x: int, y: int) -> Iterable[tuple[Widget, Region]]:
"""Get all widgets under a given coordinate.
@@ -1380,7 +1425,7 @@ class Screen(Generic[ScreenResultType], Widget):
"""Screen has suspended."""
if self.app.SUSPENDED_SCREEN_CLASS:
self.add_class(self.app.SUSPENDED_SCREEN_CLASS)
self.app._set_mouse_over(None)
self.app._set_mouse_over(None, None)
self._clear_tooltip()
self.stack_updates += 1
@@ -1492,14 +1537,17 @@ class Screen(Generic[ScreenResultType], Widget):
tooltip.update(tooltip_content)
def _handle_mouse_move(self, event: events.MouseMove) -> None:
hover_widget: Widget | None = None
try:
if self.app.mouse_captured:
widget = self.app.mouse_captured
region = self.find_widget(widget).region
else:
widget, region = self.get_widget_at(event.x, event.y)
(widget, region), hover = self.get_hover_widgets_at(event.x, event.y)
if hover is not None:
hover_widget = hover[0]
except errors.NoWidget:
self.app._set_mouse_over(None)
self.app._set_mouse_over(None, None)
if self._tooltip_timer is not None:
self._tooltip_timer.stop()
if not self.app._disable_tooltips:
@@ -1507,9 +1555,8 @@ class Screen(Generic[ScreenResultType], Widget):
self.get_child_by_type(Tooltip).display = False
except NoMatches:
pass
else:
self.app._set_mouse_over(widget)
self.app._set_mouse_over(widget, hover_widget)
widget.hover_style = event.style
if widget is self:
self.post_message(event)

View File

@@ -690,7 +690,8 @@ class Strip:
Returns:
Text with ANSI escape sequences.
"""
ansi = style._ansi or cls.render_ansi(style, color_system)
if (ansi := style._ansi) is None:
ansi = cls.render_ansi(style, color_system)
output = f"\x1b[{ansi}m{text}\x1b[0m" if ansi else text
if style._link:
output = (

View File

@@ -11,7 +11,7 @@ from rich.text import Text
from textual import events
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import ScrollableContainer
from textual.containers import HorizontalGroup, ScrollableContainer
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Label
@@ -20,6 +20,15 @@ if TYPE_CHECKING:
from textual.screen import Screen
@rich.repr.auto
class KeyGroup(HorizontalGroup):
DEFAULT_CSS = """
KeyGroup {
width: auto;
}
"""
@rich.repr.auto
class FooterKey(Widget):
ALLOW_SELECT = False
@@ -32,6 +41,7 @@ class FooterKey(Widget):
FooterKey {
width: auto;
height: 1;
text-wrap: nowrap;
background: $footer-item-background;
.footer-key--key {
color: $footer-key-foreground;
@@ -87,6 +97,7 @@ class FooterKey(Widget):
if disabled:
classes += " -disabled"
super().__init__(classes=classes)
self.shrink = False
if tooltip:
self.tooltip = tooltip
@@ -98,6 +109,7 @@ class FooterKey(Widget):
description_padding = self.get_component_styles(
"footer-key--description"
).padding
description = self.description
if description:
label_text = Text.assemble(
@@ -144,7 +156,18 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
height: 1;
scrollbar-size: 0 0;
&.-compact {
grid-gutter: 1;
FooterLabel {
margin: 0;
}
FooterKey {
margin-right: 1;
}
FooterKey.-grouped {
margin: 0 1;
}
FooterKey.-command-palette {
padding-right: 0;
}
}
FooterKey.-command-palette {
dock: right;
@@ -156,6 +179,22 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
height: 1;
layout: horizontal;
}
KeyGroup.-compact {
FooterKey.-grouped {
margin: 0;
}
margin: 0 1 0 0;
padding-left: 1;
}
FooterKey.-grouped {
margin: 0 1;
}
FooterLabel {
margin: 0 1 0 0;
color: $footer-description-foreground;
background: $footer-description-background;
}
&:ansi {
background: ansi_default;
@@ -180,19 +219,11 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
border-left: vkey ansi_black;
}
}
FooterKey.-grouped {
margin: 0 1;
}
FooterLabel {
margin: 0 1;
background: red;
color: $footer-description-foreground;
background: $footer-description-background;
}
}
"""
compact = reactive(False)
compact = reactive(False, toggle_class="-compact")
"""Display in compact style."""
_bindings_ready = reactive(False, repaint=False)
"""True if the bindings are ready to be displayed."""
@@ -209,6 +240,7 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
classes: str | None = None,
disabled: bool = False,
show_command_palette: bool = True,
compact: bool = False,
) -> None:
"""A footer to show key bindings.
@@ -219,6 +251,7 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
classes: The CSS classes for the widget.
disabled: Whether the widget is disabled or not.
show_command_palette: Show key binding to invoke the command palette, on the right of the footer.
compact: Display a compact style (less whitespace) footer.
"""
super().__init__(
*children,
@@ -228,6 +261,7 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
disabled=disabled,
)
self.set_reactive(Footer.show_command_palette, show_command_palette)
self.compact = compact
def compose(self) -> ComposeResult:
if not self._bindings_ready:
@@ -247,23 +281,25 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
for group, multi_bindings_iterable in groupby(
action_to_bindings.values(),
lambda multi_bindings: multi_bindings[0][0].group,
lambda multi_bindings_: multi_bindings_[0][0].group,
):
if group is not None:
for multi_bindings in multi_bindings_iterable:
binding, enabled, tooltip = multi_bindings[0]
yield FooterKey(
binding.key,
self.app.get_key_display(binding),
"",
binding.action,
disabled=not enabled,
tooltip=tooltip or binding.description,
classes="-grouped",
).data_bind(Footer.compact)
multi_bindings = list(multi_bindings_iterable)
if group is not None and len(multi_bindings) > 1:
with KeyGroup(classes="-compact" if group.compact else ""):
for multi_bindings in multi_bindings:
binding, enabled, tooltip = multi_bindings[0]
yield FooterKey(
binding.key,
self.app.get_key_display(binding),
"",
binding.action,
disabled=not enabled,
tooltip=tooltip or binding.description,
classes="-grouped",
).data_bind(compact=Footer.compact)
yield FooterLabel(group.description)
else:
for multi_bindings in multi_bindings_iterable:
for multi_bindings in multi_bindings:
binding, enabled, tooltip = multi_bindings[0]
yield FooterKey(
binding.key,
@@ -272,7 +308,7 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
binding.action,
disabled=not enabled,
tooltip=tooltip,
).data_bind(Footer.compact)
).data_bind(compact=Footer.compact)
if self.show_command_palette and self.app.ENABLE_COMMAND_PALETTE:
try:
_node, binding, enabled, tooltip = active_bindings[
@@ -324,6 +360,3 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
def on_unmount(self) -> None:
self.screen.bindings_updated_signal.unsubscribe(self)
def watch_compact(self, compact: bool) -> None:
self.set_class(compact, "-compact")

View File

@@ -121,7 +121,7 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#121212" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="25.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="269.9" width="402.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="402.6" y="269.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="573.4" y="269.9" width="402.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#2e3841" x="0" y="562.7" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#2e3841" x="24.4" y="562.7" width="268.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="292.8" y="562.7" width="536.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="829.6" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="841.8" y="562.7" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="866.2" y="562.7" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="963.8" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#121212" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="25.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="269.9" width="402.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="402.6" y="269.9" width="170.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="573.4" y="269.9" width="402.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#39434b" x="0" y="562.7" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#39434b" x="24.4" y="562.7" width="268.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="292.8" y="562.7" width="536.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="829.6" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="841.8" y="562.7" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="866.2" y="562.7" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="963.8" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -121,7 +121,7 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#121212" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="25.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="269.9" width="390.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="390.4" y="269.9" width="183" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="573.4" y="269.9" width="402.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#2e3841" x="0" y="562.7" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#2e3841" x="48.8" y="562.7" width="268.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="317.2" y="562.7" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="829.6" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="841.8" y="562.7" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="866.2" y="562.7" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="963.8" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#121212" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="25.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="269.9" width="390.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="390.4" y="269.9" width="183" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="573.4" y="269.9" width="402.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#39434b" x="0" y="562.7" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#39434b" x="48.8" y="562.7" width="268.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="317.2" y="562.7" width="512.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="829.6" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="841.8" y="562.7" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="866.2" y="562.7" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="963.8" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB