Skip to content

Windows Terminal Border Collision #6327

@EmreDemircan

Description

@EmreDemircan

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()

Image

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions