Repository: Textualize/textual

Issue:
Report a bug about switch_screen
```python
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Static, Header, Footer

class ScreenA(Screen):
    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("A")
        yield Footer()

class ScreenB(Screen):
    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("B")
        yield Footer()

class ModalApp(App):
    SCREENS = {
        "a": ScreenA(),
        "b": ScreenB(),
    }

    BINDINGS = [
        ("1", "switch_screen('a')", "A"),
        ("2", "switch_screen('b')", "B"),
    ]

    def on_mount(self) -> None:
        self.push_screen("a")


if __name__ == "__main__":
    app = ModalApp()
    app.run()
```

This is sample code. When '21' pressed, it will crash. Log below:
```
╭──────────────────────────────────────────────────────────────────── Traceback (most recent call last) ─────────────────────────────────────────────────────────────────────╮
│ /home/tsing/.local/lib/python3.10/site-packages/textual/app.py:2488 in action_switch_screen                                                                                │
│                                                                                                                                                                            │
│   2485 │   │   Args:                                                                            ╭────────────────────────── locals ───────────────────────────╮            │
│   2486 │   │   │   screen: Name of the screen.                                                  │ screen = 'a'                                                │            │
│   2487 │   │   """                                                                              │   self = ModalApp(title='ModalApp', classes={'-dark-mode'}) │            │
│ ❱ 2488 │   │   self.switch_screen(screen)                                                       ╰─────────────────────────────────────────────────────────────╯            │
│   2489 │                                                                                                                                                                   │
│   2490 │   async def action_push_screen(self, screen: str) -> None:                                                                                                        │
│   2491 │   │   """An [action](/guide/actions) to push a new screen on to the stack and make it                                                                             │
│                                                                                                                                                                            │
│ /home/tsing/.local/lib/python3.10/site-packages/textual/app.py:1446 in switch_screen                                                                                       │
│                                                                                                                                                                            │
│   1443 │   │   │   )                                                                            ╭─────────────────────────────── locals ───────────────────────────────╮   │
│   1444 │   │   if self.screen is not screen:                                                    │ previous_screen = ScreenB(pseudo_classes={'enabled'})                │   │
│   1445 │   │   │   previous_screen = self._replace_screen(self._screen_stack.pop())             │          screen = 'a'                                                │   │
│ ❱ 1446 │   │   │   previous_screen._pop_result_callback()                                       │            self = ModalApp(title='ModalApp', classes={'-dark-mode'}) │   │
│   1447 │   │   │   next_screen, await_mount = self._get_screen(screen)                          ╰──────────────────────────────────────────────────────────────────────╯   │
│   1448 │   │   │   self._screen_stack.append(next_screen)                                                                                                                  │
│   1449 │   │   │   self.screen.post_message(events.ScreenResume())                                                                                                         │
│                                                                                                                                                                            │
│ /home/tsing/.local/lib/python3.10/site-packages/textual/screen.py:572 in _pop_result_callback                                                                              │
│                                                                                                                                                                            │
│   569 │                                                                                        ╭────────────────── locals ──────────────────╮                              │
│   570 │   def _pop_result_callback(self) -> None:                                              │ self = ScreenB(pseudo_classes={'enabled'}) │                              │
│   571 │   │   """Remove the latest result callback from the stack."""                          ╰────────────────────────────────────────────╯                              │
│ ❱ 572 │   │   self._result_callbacks.pop()                                                                                                                                 │
│   573 │                                                                                                                                                                    │
│   574 │   def _refresh_layout(                                                                                                                                             │
│   575 │   │   self, size: Size | None = None, full: bool = False, scroll: bool = False                                                                                     │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
IndexError: pop from empty list
```

The direct reason is `_ push_ result_ callback` not called when `switch_screen`, or check `self._result_callbacks` is empty when pop.


Discussion:
Thank you for your issue. Give us a little time to review it.

PS. You might want to check the [FAQ](https://github.com/textualize/textual/blob/main/FAQ.md) if you haven't done so already.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)
Same thing happened to me. I'm switching a screen that another screen has a callback on. 
Thanks for opening this issue @tsingwang.

@chitter99 can you add a bit more context to _your_ specific problem, please? Ideally with a minimal reproducible example?
It's likely that the issues are related but a code sample from you could be quite helpful.
Studying this a bit further revealed that there was another similar issue because switching to the currently active screen should be a no-op but it wasn't.

Resolving patch:
diff --git a/src/textual/app.py b/src/textual/app.py
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -1611,15 +1611,19 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount:
             raise TypeError(
                 f"switch_screen requires a Screen instance or str; not {screen!r}"
             )
-        if self.screen is not screen:
-            previous_screen = self._replace_screen(self._screen_stack.pop())
-            previous_screen._pop_result_callback()
-            next_screen, await_mount = self._get_screen(screen)
-            self._screen_stack.append(next_screen)
-            self.screen.post_message(events.ScreenResume())
-            self.log.system(f"{self.screen} is current (SWITCHED)")
-            return await_mount
-        return AwaitMount(self.screen, [])
+
+        next_screen, await_mount = self._get_screen(screen)
+        if screen is self.screen or next_screen is self.screen:
+            self.log.system(f"Screen {screen} is already current.")
+            return AwaitMount(self.screen, [])
+
+        previous_screen = self._replace_screen(self._screen_stack.pop())
+        previous_screen._pop_result_callback()
+        self._screen_stack.append(next_screen)
+        self.screen.post_message(events.ScreenResume())
+        self.screen._push_result_callback(self.screen, None)
+        self.log.system(f"{self.screen} is current (SWITCHED)")
+        return await_mount
 
     def install_screen(self, screen: Screen, name: str) -> None:
         """Install a screen.


Test patch:
diff --git a/tests/test_screens.py b/tests/test_screens.py
--- a/tests/test_screens.py
+++ b/tests/test_screens.py
@@ -298,3 +298,55 @@ async def key_p(self) -> None:
         await pilot.press("p")
         with pytest.raises(ScreenStackError):
             app.bottom.dismiss()
+
+
+async def test_switch_screen_no_op():
+    """Regression test for https://github.com/Textualize/textual/issues/2650"""
+
+    class MyScreen(Screen):
+        pass
+
+    class MyApp(App[None]):
+        SCREENS = {"screen": MyScreen()}
+
+        def on_mount(self):
+            self.push_screen("screen")
+
+    app = MyApp()
+    async with app.run_test():
+        screen_id = id(app.screen)
+        app.switch_screen("screen")
+        assert screen_id == id(app.screen)
+        app.switch_screen("screen")
+        assert screen_id == id(app.screen)
+
+
+async def test_switch_screen_updates_results_callback_stack():
+    """Regression test for https://github.com/Textualize/textual/issues/2650"""
+
+    class ScreenA(Screen):
+        pass
+
+    class ScreenB(Screen):
+        pass
+
+    class MyApp(App[None]):
+        SCREENS = {
+            "a": ScreenA(),
+            "b": ScreenB(),
+        }
+
+        def callback(self, _):
+            return 42
+
+        def on_mount(self):
+            self.push_screen("a", self.callback)
+
+    app = MyApp()
+    async with app.run_test():
+        assert len(app.screen._result_callbacks) == 1
+        assert app.screen._result_callbacks[-1].callback(None) == 42
+
+        app.switch_screen("b")
+        assert len(app.screen._result_callbacks) == 1
+        assert app.screen._result_callbacks[-1].callback is None