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:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7de004cfe..03f7142ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 - Fixed zero division error https://github.com/Textualize/textual/issues/2673
 - Fix `scroll_to_center` when there were nested layers out of view (Compositor full_map not populated fully) https://github.com/Textualize/textual/pull/2684
+- Issues with `switch_screen` not updating the results callback appropriately https://github.com/Textualize/textual/issues/2650
 
 ### Added
 
@@ -22,7 +23,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 - `SuggestFromList` class to let widgets get completions from a fixed set of options https://github.com/Textualize/textual/pull/2604
 - `Input` has a new component class `input--suggestion` https://github.com/Textualize/textual/pull/2604
 - Added `Widget.remove_children` https://github.com/Textualize/textual/pull/2657
-- Added `Validator` framework and validation for `Input` https://github.com/Textualize/textual/pull/2600
 
 ### Changed
 
@@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 - `Tree` and `DirectoryTree` Messages no longer accept a `tree` parameter, using `self.node.tree` instead. https://github.com/Textualize/textual/issues/2529
 - Keybinding <kbd>right</kbd> in `Input` is also used to accept a suggestion if the cursor is at the end of the input https://github.com/Textualize/textual/pull/2604
 - `Input.__init__` now accepts a `suggester` attribute for completion suggestions https://github.com/Textualize/textual/pull/2604
+- Using `switch_screen` to switch to the currently active screen is now a no-op https://github.com/Textualize/textual/pull/2692
 
 ### Removed
 
diff --git a/src/textual/app.py b/src/textual/app.py
index 879edbad5..e9300f8c8 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -1611,15 +1611,19 @@ class App(Generic[ReturnType], DOMNode):
             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
index 033891990..7f42a21a2 100644
--- a/tests/test_screens.py
+++ b/tests/test_screens.py
@@ -298,3 +298,55 @@ async def test_dismiss_non_top_screen():
         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