-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Consider discussions!
Issues are for actionable items only.
This issue describes a layout/rendering bug where borders visually overlap.
The bug
Widget borders visually overlap / intersect when multiple bordered containers are placed side-by-side in a layout.
The issue occurs even though widgets do not logically overlap and have distinct sizes.
This results in broken UI rendering where vertical borders from adjacent widgets intersect or overwrite each other.
Expected behavior
Each widget should render its border independently, with clean separation between neighboring widgets.
Borders should not overlap or collide when widgets are laid out using Grid/Dock.
Actual behavior
Borders intersect at shared edges, causing double lines, clipped corners, or visually merged borders.
Example code:
# -*- coding:utf-8 -*-
"""Textual border overlap repro - dashboard layout"""
from datetime import datetime
from typing import Dict, List
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical, VerticalScroll
from textual.widgets import Static, ProgressBar, Input, Button
# Dummy events data
DUMMY_EVENTS: List[Dict[str, str]] = [
{"time": "14:56", "type": "info", "icon": "🔄", "message": "Refreshing token..."},
{"time": "14:56", "type": "warning", "icon": "⚠️", "message": "Token expired, renewing..."},
]
COMMANDS = {
"/token": "Show current token details (dummy)",
"/status": "Show system status (dummy)",
"/clear": "Clear event history",
"/help": "List available commands",
"/ws": "Show WebSocket connection info (dummy)",
"/user": "Show user information (dummy)",
}
class StatusCard(Static):
"""Status card widget"""
def __init__(self, title: str, icon: str = "●", status: str = "...", variant: str = "default"):
super().__init__()
self.card_title = title
self.icon = icon
self.status_text = status
self.variant = variant
def compose(self) -> ComposeResult:
yield Static(f"{self.icon} {self.card_title}", classes="card-title")
yield Static(self.status_text, classes="card-status")
def on_mount(self) -> None:
status_widget = self.query_one(".card-status", Static)
if self.variant == "success":
status_widget.update(f"[green]{self.status_text}[/green]")
elif self.variant == "warning":
status_widget.update(f"[yellow]{self.status_text}[/yellow]")
elif self.variant == "error":
status_widget.update(f"[red]{self.status_text}[/red]")
else:
status_widget.update(self.status_text)
class ActiveTaskWidget(Static):
"""Active task display widget"""
def __init__(self, task_id: str, name: str, progress: int = 0):
super().__init__()
self.task_id = task_id
self.task_name = name
self.progress = progress
def compose(self) -> ComposeResult:
yield Static(f"⏳ {self.task_name}", classes="task-name")
yield ProgressBar(total=100, show_eta=False)
def on_mount(self) -> None:
self.query_one(ProgressBar).advance(self.progress)
class EventItem(Static):
"""Single event row"""
def __init__(self, event: Dict[str, str]):
super().__init__()
self.event = event
def compose(self) -> ComposeResult:
event = self.event
color = {"info": "cyan", "success": "green", "warning": "yellow", "error": "red"}.get(event["type"], "white")
yield Static(f"[dim]{event['time']}[/dim] {event['icon']} [{color}]{event['message']}[/{color}]")
class QuickCommandButton(Button):
"""Quick command button"""
def __init__(self, command: str, label: str):
super().__init__(label)
self.command = command
class DashboardScreen(Container):
"""Main dashboard screen"""
def compose(self) -> ComposeResult:
with Vertical(id="root"):
# Main (top) area
with Horizontal(classes="dashboard-main"):
# Left panel
with Vertical(classes="left-panel"):
with Vertical(classes="status-section"):
yield Static("📊 System Status", classes="section-header")
yield StatusCard("WebSocket", "🔌", "Connecting...", "warning")
yield StatusCard("Authentication", "🔐", "Checking token...", "warning")
yield StatusCard("Database", "🗄️", "Ready", "success")
with Vertical(classes="events-section"):
yield Static("📋 Recent Events", classes="section-header")
with VerticalScroll(id="events-container"):
for event in DUMMY_EVENTS:
yield EventItem(event)
# Right panel
with Vertical(classes="tasks-section"):
yield Static("⚙️ Active Tasks", classes="section-header")
yield Static("[dim]No active tasks[/dim]", id="no-tasks")
yield Container(id="active-tasks-container")
# Bottom command area
with Vertical(classes="command-section"):
yield Static("⌨️ Command Console", classes="section-header")
with Horizontal(classes="command-buttons"):
yield QuickCommandButton("/help", "❓ /help")
yield QuickCommandButton("/token", "🔑 /token")
yield QuickCommandButton("/status", "📊 /status")
yield QuickCommandButton("/user", "👤 /user")
yield QuickCommandButton("/ws", "🔌 /ws")
yield QuickCommandButton("/clear", "🗑️ /clear")
yield Input(placeholder="Enter a command (e.g. /help, /token)...", id="command-input")
yield Static("", id="command-output", classes="command-output")
def on_mount(self) -> None:
self.query_one("#command-output", Static).display = False
def on_button_pressed(self, event: Button.Pressed) -> None:
if isinstance(event.button, QuickCommandButton):
self._execute_command(event.button.command)
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "command-input":
command = event.value.strip()
if command:
self._execute_command(command)
event.input.value = ""
def _execute_command(self, command: str) -> None:
output_widget = self.query_one("#command-output", Static)
if command == "/clear":
container = self.query_one("#events-container", VerticalScroll)
container.remove_children()
container.mount(Static("[dim]Event history cleared[/dim]"))
output_widget.display = False
return
output_widget.display = True
if command == "/help":
lines = ["[bold cyan]📖 Available Commands:[/bold cyan]\n"]
for cmd, desc in COMMANDS.items():
lines.append(f" [green]{cmd}[/green] - {desc}")
output_widget.update("\n".join(lines))
elif command == "/status":
output_widget.update(
"\n".join(
[
"[bold cyan]📊 System Status (dummy):[/bold cyan]\n",
" [dim]WebSocket:[/dim] [yellow]Connecting...[/yellow]",
f" [dim]Time:[/dim] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
]
)
)
else:
output_widget.update(f"[dim]Dummy output for[/dim] [cyan]{command}[/cyan]")
class BorderOverlapRepro(App):
TITLE = "Border Overlap Repro"
CSS = r"""
#root {
height: 100%;
width: 100%;
}
.dashboard-main {
height: 1fr;
width: 100%;
}
/* Left / right panels aynı */
.left-panel {
width: 45%;
height: 100%;
border: solid #00a2ff;
padding: 1 1;
}
.tasks-section {
width: 55%;
height: 100%;
border: solid #00ffd5;
padding: 1 1;
}
.status-section {
border: solid #00a2ff;
padding: 1 1;
height: auto;
}
.events-section {
border: solid #00ff66;
padding: 1 1;
height: 1fr;
margin-top: 1;
}
#events-container {
height: 1fr;
}
/* ✅ Kompakt ama border'ı bozmayacak şekilde */
.command-section {
border: solid #2c2c2c;
padding: 0 1;
height: 5; /* <- kritik: sabit küçük yükseklik */
}
/* Başlık tek satır, boşluk yok */
.command-section .section-header {
height: 1;
margin: 0 0;
}
/* Buton satırı kompakt */
.command-buttons {
height: 1;
margin: 0 0;
}
Button {
height: 1;
padding: 0 1;
}
/* Input tek satır */
Input {
height: 1;
margin: 0 0;
}
/* Output tek satır (veya istersen tamamen kapat) */
.command-output {
height: 1;
margin: 0 0;
}
"""
def compose(self) -> ComposeResult:
yield DashboardScreen()
if __name__ == "__main__":
BorderOverlapRepro().run()
The issue is intermittent. In my main project it occurs most of the time, but toggling fullscreen, resizing the terminal, or repeating these actions can change the outcome: sometimes the borders render correctly, sometimes they overlap again.
I am using the latest Windows Terminal from the Microsoft Store on Windows 11. The active font is Cascadia Mono.