Skip to main content

muxr_client/pane/
mouse.rs

1use muxr_config::MuxrConfig;
2use muxr_core::ClientMouseEvent;
3use muxr_core::ClientMouseEventPhase;
4use muxr_core::ClientMousePosition;
5use muxr_core::ClientRequest;
6
7use crate::copy_selection::SelectionInput;
8use crate::pane::focus::LocalMouseAction;
9use crate::renderer::ClientRenderer;
10use crate::renderer::FileOpenRelease;
11use crate::runtime::ClientInputSend;
12use crate::runtime::DroppableSendOutcome;
13
14pub async fn handle_mouse_input_action(
15    muxr_config: &MuxrConfig,
16    event: ClientMouseEvent,
17    input_sender: &tokio::sync::mpsc::Sender<ClientRequest>,
18    renderer: &mut ClientRenderer,
19) -> rootcause::Result<ClientInputSend> {
20    if matches!(self::alt_left_click_phase(event), Some(AltLeftClickPhase::Release))
21        && matches!(renderer.finish_file_open_capture(), FileOpenRelease::Consumed)
22    {
23        return Ok(ClientInputSend::Accepted);
24    }
25    let Some(position) = pane_position(muxr_config, event.position) else {
26        if let Some(request) = renderer.tab_focus_request_for_sidebar_click(event) {
27            if input_sender.send(request).await.is_err() {
28                return Ok(ClientInputSend::Closed);
29            }
30            return Ok(ClientInputSend::Accepted);
31        }
32        // Captured app drags can finish over the tab bar; forward them clamped to the captured pane before dropping
33        // ordinary tab bar mouse packets.
34        if renderer.mouse_capture_state() == crate::renderer::MouseCaptureState::Captured
35            && let Some(position) = pane_position_for_sidebar_drag(muxr_config, event.position)
36            && let Some(event) = renderer.mouse_request_for_event(ClientMouseEvent { position, ..event })
37        {
38            return send_mouse_request(input_sender, event).await;
39        }
40        // Local selections can also finish over the tab bar; keep update/end routed so the retained pane drag is
41        // clamped and finalized instead of leaving stale drag state behind.
42        let tab_bar_position = event.position;
43        match crate::pane::focus::LocalMouseAction::from_event(event) {
44            Some(LocalMouseAction::SelectionUpdate(_)) => {
45                if let Some(position) = pane_position_for_sidebar_drag(muxr_config, tab_bar_position) {
46                    let scroll_request = renderer.set_selection_outside_edge_drag(position);
47                    renderer.apply_selection_input_logical(SelectionInput::Update(position))?;
48                    if let Some(request) = scroll_request {
49                        return Ok(crate::runtime::send_edge_scroll_request(
50                            input_sender,
51                            renderer,
52                            request,
53                        ));
54                    }
55                }
56            }
57            Some(LocalMouseAction::SelectionEnd(_)) => {
58                if let Some(position) = pane_position_for_sidebar_drag(muxr_config, tab_bar_position) {
59                    renderer.apply_selection_input_logical(SelectionInput::End(position))?;
60                }
61            }
62            Some(LocalMouseAction::FocusAndSelectionStart(_)) | None => {}
63        }
64        return Ok(ClientInputSend::Accepted);
65    };
66    let event = ClientMouseEvent { position, ..event };
67    if crate::pane::scroll::MouseWheelEvent::from(event) == crate::pane::scroll::MouseWheelEvent::Wheel {
68        return send_mouse_request(input_sender, event).await;
69    }
70    if matches!(self::alt_left_click_phase(event), Some(AltLeftClickPhase::Press))
71        && let Some(request) = renderer.file_open_request(position)
72    {
73        if input_sender.send(request).await.is_err() {
74            return Ok(ClientInputSend::Closed);
75        }
76        renderer.begin_file_open_capture();
77        return Ok(ClientInputSend::Accepted);
78    }
79    if let Some(event) = renderer.mouse_request_for_event(event) {
80        return send_mouse_request(input_sender, event).await;
81    }
82
83    match crate::pane::focus::LocalMouseAction::from_event(event) {
84        Some(LocalMouseAction::FocusAndSelectionStart(position)) => {
85            if input_sender.send(ClientRequest::FocusPaneAt(position)).await.is_err() {
86                return Ok(ClientInputSend::Closed);
87            }
88            renderer.apply_selection_input_logical(SelectionInput::Start(position))?;
89            Ok(ClientInputSend::Accepted)
90        }
91        Some(LocalMouseAction::SelectionUpdate(position)) => {
92            let scroll_request = renderer.set_selection_edge_drag(position, None);
93            renderer.apply_selection_input_logical(SelectionInput::Update(position))?;
94            if let Some(request) = scroll_request {
95                return Ok(crate::runtime::send_edge_scroll_request(
96                    input_sender,
97                    renderer,
98                    request,
99                ));
100            }
101            Ok(ClientInputSend::Accepted)
102        }
103        Some(LocalMouseAction::SelectionEnd(position)) => {
104            renderer.apply_selection_input_logical(SelectionInput::End(position))?;
105            Ok(ClientInputSend::Accepted)
106        }
107        None => Ok(ClientInputSend::Accepted),
108    }
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112enum AltLeftClickPhase {
113    Press,
114    Release,
115}
116
117const fn alt_left_click_phase(event: ClientMouseEvent) -> Option<AltLeftClickPhase> {
118    if event.button != 8 {
119        return None;
120    }
121    match event.phase {
122        ClientMouseEventPhase::Press => Some(AltLeftClickPhase::Press),
123        ClientMouseEventPhase::Release => Some(AltLeftClickPhase::Release),
124    }
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub enum MouseEventDrop {
129    Droppable,
130    Required,
131}
132
133impl From<ClientMouseEvent> for MouseEventDrop {
134    fn from(event: ClientMouseEvent) -> Self {
135        if event.button & 32 != 0
136            && !matches!(
137                crate::pane::scroll::MouseWheelEvent::from(event),
138                crate::pane::scroll::MouseWheelEvent::Wheel
139            )
140        {
141            Self::Droppable
142        } else {
143            Self::Required
144        }
145    }
146}
147
148async fn send_mouse_request(
149    input_sender: &tokio::sync::mpsc::Sender<ClientRequest>,
150    event: ClientMouseEvent,
151) -> rootcause::Result<ClientInputSend> {
152    if MouseEventDrop::from(event) == MouseEventDrop::Droppable {
153        return Ok(
154            match crate::runtime::send_droppable_request(input_sender, ClientRequest::Mouse(event)) {
155                DroppableSendOutcome::Closed => ClientInputSend::Closed,
156                DroppableSendOutcome::Dropped | DroppableSendOutcome::Sent => ClientInputSend::Accepted,
157            },
158        );
159    }
160    if input_sender.send(ClientRequest::Mouse(event)).await.is_err() {
161        return Ok(ClientInputSend::Closed);
162    }
163    Ok(ClientInputSend::Accepted)
164}
165
166fn pane_position(config: &MuxrConfig, position: ClientMousePosition) -> Option<ClientMousePosition> {
167    Some(ClientMousePosition {
168        row: position.row,
169        col: position.col.checked_sub(config.tab_bar.width)?,
170    })
171}
172
173const fn pane_position_for_sidebar_drag(
174    config: &MuxrConfig,
175    position: ClientMousePosition,
176) -> Option<ClientMousePosition> {
177    if position.col >= config.tab_bar.width {
178        return None;
179    }
180    Some(ClientMousePosition {
181        row: position.row,
182        col: 0,
183    })
184}
185
186#[cfg(test)]
187mod tests {
188    use muxr_core::LayoutSnapshot;
189    use muxr_core::PaneId;
190    use muxr_core::PaneRegionsSnapshot;
191    use muxr_core::PaneSnapshot;
192    use muxr_core::TabId;
193    use muxr_core::TabSnapshot;
194    use muxr_core::TerminalSize;
195    use rootcause::prelude::ResultExt;
196    use rootcause::report;
197    use test_that::prelude::*;
198
199    use super::*;
200    use crate::renderer::ClientRenderer;
201    use crate::renderer::test_helpers as renderer_test_helpers;
202    use crate::terminal::SynchronizedOutput;
203
204    #[test]
205    fn test_handle_mouse_input_action_when_plain_mouse_click_arrives_focuses_pane() -> rootcause::Result<()> {
206        self::runtime()?.block_on(async {
207            let config = MuxrConfig::default();
208            let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
209            let mut renderer = ClientRenderer::with_synchronized_output(
210                self::layout_snapshot()?,
211                self::pane_regions_snapshot()?,
212                SynchronizedOutput::Csi,
213            );
214            assert_that!(
215                handle_mouse_input_action(
216                    &config,
217                    ClientMouseEvent {
218                        button: 0,
219                        phase: ClientMouseEventPhase::Press,
220                        position: ClientMousePosition {
221                            row: 0,
222                            col: config.tab_bar.width.saturating_add(1)
223                        }
224                    },
225                    &input_sender,
226                    &mut renderer,
227                )
228                .await?,
229                eq(ClientInputSend::Accepted)
230            );
231
232            assert_that!(
233                input_receiver.recv().await,
234                eq(Some(ClientRequest::FocusPaneAt(ClientMousePosition { row: 0, col: 1 })))
235            );
236            Ok(())
237        })
238    }
239
240    #[test]
241    fn test_handle_mouse_input_action_when_alt_click_hits_file_path_sends_one_open_file_request()
242    -> rootcause::Result<()> {
243        self::runtime()?.block_on(async {
244            let config = MuxrConfig::default();
245            let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(2);
246            let mut renderer = ClientRenderer::with_synchronized_output(
247                self::layout_snapshot()?,
248                self::file_link_pane_regions_snapshot()?,
249                SynchronizedOutput::Csi,
250            );
251            renderer.apply_render_logical(muxr_core::RenderUpdate::Baseline(self::file_link_render_baseline()?))?;
252            assert_that!(
253                handle_mouse_input_action(
254                    &config,
255                    ClientMouseEvent {
256                        button: 8,
257                        phase: ClientMouseEventPhase::Press,
258                        position: ClientMousePosition {
259                            row: 0,
260                            col: config.tab_bar.width.saturating_add(5),
261                        },
262                    },
263                    &input_sender,
264                    &mut renderer,
265                )
266                .await?,
267                eq(ClientInputSend::Accepted)
268            );
269
270            assert_that!(
271                input_receiver.recv().await,
272                eq(Some(ClientRequest::OpenFile {
273                    pane_id: PaneId::new(1)?,
274                    path: "/tmp/foo.rs".to_owned(),
275                    line: Some(42),
276                    column: Some(7),
277                }))
278            );
279            assert_that!(
280                input_receiver.try_recv(),
281                err(matches_pattern!(tokio::sync::mpsc::error::TryRecvError::Empty))
282            );
283
284            let mut decoder = crate::input::InputDecoder::default();
285            let decoded_release = decoder.decode(b"\x1b[<8;10;5m");
286            let [crate::input::DecodedInput::Mouse(release)] = decoded_release.as_slice() else {
287                return Err(report!("expected decoded Alt-left release"));
288            };
289            assert_that!(
290                handle_mouse_input_action(&config, *release, &input_sender, &mut renderer).await?,
291                eq(ClientInputSend::Accepted)
292            );
293            assert_that!(
294                input_receiver.try_recv(),
295                err(matches_pattern!(tokio::sync::mpsc::error::TryRecvError::Empty))
296            );
297            Ok(())
298        })
299    }
300
301    #[test]
302    fn test_alt_left_click_phase_when_extra_modifiers_are_present_ignores_event() {
303        for button in [12, 24, 28] {
304            assert_that!(
305                alt_left_click_phase(ClientMouseEvent {
306                    button,
307                    phase: ClientMouseEventPhase::Press,
308                    position: ClientMousePosition { row: 0, col: 0 },
309                }),
310                none()
311            );
312        }
313    }
314
315    #[test]
316    fn test_handle_mouse_input_action_when_tab_sidebar_is_clicked_focuses_tab() -> rootcause::Result<()> {
317        self::runtime()?.block_on(async {
318            let config = MuxrConfig::default();
319            let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
320            let mut renderer = ClientRenderer::with_synchronized_output(
321                self::two_tab_layout()?,
322                self::pane_regions_snapshot()?,
323                SynchronizedOutput::Csi,
324            );
325            assert_that!(
326                handle_mouse_input_action(
327                    &config,
328                    ClientMouseEvent {
329                        button: 0,
330                        phase: ClientMouseEventPhase::Press,
331                        position: ClientMousePosition { row: 3, col: 1 }
332                    },
333                    &input_sender,
334                    &mut renderer,
335                )
336                .await?,
337                eq(ClientInputSend::Accepted)
338            );
339
340            assert_that!(
341                input_receiver.recv().await,
342                eq(Some(ClientRequest::FocusTab(TabId::new(2)?)))
343            );
344            Ok(())
345        })
346    }
347
348    #[test]
349    fn test_handle_mouse_input_action_when_selection_release_is_on_tab_sidebar_finalizes_selection()
350    -> rootcause::Result<()> {
351        self::runtime()?.block_on(async {
352            let config = MuxrConfig::default();
353            let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
354            let mut renderer = ClientRenderer::with_synchronized_output(
355                self::layout_snapshot()?,
356                self::pane_regions_snapshot()?,
357                SynchronizedOutput::Csi,
358            );
359            renderer.apply_render_logical(muxr_core::RenderUpdate::Baseline(self::render_baseline()?))?;
360            assert_that!(
361                handle_mouse_input_action(
362                    &config,
363                    ClientMouseEvent {
364                        button: 0,
365                        phase: ClientMouseEventPhase::Press,
366                        position: ClientMousePosition {
367                            row: 0,
368                            col: config.tab_bar.width.saturating_add(1)
369                        }
370                    },
371                    &input_sender,
372                    &mut renderer,
373                )
374                .await?,
375                eq(ClientInputSend::Accepted)
376            );
377            assert_that!(
378                handle_mouse_input_action(
379                    &config,
380                    ClientMouseEvent {
381                        button: 0,
382                        phase: ClientMouseEventPhase::Release,
383                        position: ClientMousePosition { row: 0, col: 1 }
384                    },
385                    &input_sender,
386                    &mut renderer,
387                )
388                .await?,
389                eq(ClientInputSend::Accepted)
390            );
391
392            assert_that!(
393                input_receiver.recv().await,
394                eq(Some(ClientRequest::FocusPaneAt(ClientMousePosition { row: 0, col: 1 })))
395            );
396            assert_that!(
397                renderer_test_helpers::selected_text(&renderer),
398                eq(Some("ab".to_owned()))
399            );
400            Ok(())
401        })
402    }
403
404    #[test]
405    fn test_handle_mouse_input_action_when_selection_drag_moves_into_tab_sidebar_clamps_to_left_edge()
406    -> rootcause::Result<()> {
407        self::runtime()?.block_on(async {
408            let config = MuxrConfig::default();
409            let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(2);
410            let mut renderer = ClientRenderer::with_synchronized_output(
411                self::layout_snapshot()?,
412                self::pane_regions_snapshot()?,
413                SynchronizedOutput::Csi,
414            );
415            renderer.apply_render_logical(muxr_core::RenderUpdate::Baseline(self::render_baseline()?))?;
416            assert_that!(
417                handle_mouse_input_action(
418                    &config,
419                    ClientMouseEvent {
420                        button: 0,
421                        phase: ClientMouseEventPhase::Press,
422                        position: ClientMousePosition {
423                            row: 0,
424                            col: config.tab_bar.width.saturating_add(1)
425                        }
426                    },
427                    &input_sender,
428                    &mut renderer,
429                )
430                .await?,
431                eq(ClientInputSend::Accepted)
432            );
433            assert_that!(
434                handle_mouse_input_action(
435                    &config,
436                    ClientMouseEvent {
437                        button: 32,
438                        phase: ClientMouseEventPhase::Press,
439                        position: ClientMousePosition { row: 0, col: 0 }
440                    },
441                    &input_sender,
442                    &mut renderer,
443                )
444                .await?,
445                eq(ClientInputSend::Accepted)
446            );
447
448            assert_that!(
449                self::recv_client_request(&mut input_receiver).await?,
450                eq(Some(ClientRequest::FocusPaneAt(ClientMousePosition { row: 0, col: 1 })))
451            );
452            assert_that!(
453                input_receiver.try_recv(),
454                err(matches_pattern!(tokio::sync::mpsc::error::TryRecvError::Empty))
455            );
456            assert_that!(
457                renderer_test_helpers::selected_text(&renderer),
458                eq(Some("ab".to_owned()))
459            );
460            Ok(())
461        })
462    }
463
464    #[test]
465    fn test_handle_mouse_input_action_when_pane_tracks_mouse_forwards_mouse_to_server() -> rootcause::Result<()> {
466        self::runtime()?.block_on(async {
467            let config = MuxrConfig::default();
468            let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
469            let mut renderer = ClientRenderer::with_synchronized_output(
470                self::layout_snapshot()?,
471                self::mouse_tracking_pane_regions_snapshot()?,
472                SynchronizedOutput::Csi,
473            );
474            assert_that!(
475                handle_mouse_input_action(
476                    &config,
477                    ClientMouseEvent {
478                        button: 0,
479                        phase: ClientMouseEventPhase::Press,
480                        position: ClientMousePosition {
481                            row: 0,
482                            col: config.tab_bar.width.saturating_add(1)
483                        }
484                    },
485                    &input_sender,
486                    &mut renderer,
487                )
488                .await?,
489                eq(ClientInputSend::Accepted)
490            );
491
492            assert_that!(
493                input_receiver.recv().await,
494                eq(Some(ClientRequest::Mouse(ClientMouseEvent {
495                    button: 0,
496                    phase: ClientMouseEventPhase::Press,
497                    position: ClientMousePosition { row: 0, col: 1 }
498                })))
499            );
500            Ok(())
501        })
502    }
503
504    #[test]
505    fn test_handle_mouse_input_action_when_pane_receives_wheel_forwards_mouse_to_server() -> rootcause::Result<()> {
506        self::runtime()?.block_on(async {
507            let config = MuxrConfig::default();
508            let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
509            let mut renderer = ClientRenderer::with_synchronized_output(
510                self::layout_snapshot()?,
511                self::pane_regions_snapshot()?,
512                SynchronizedOutput::Csi,
513            );
514            let event = ClientMouseEvent {
515                button: 64,
516                phase: ClientMouseEventPhase::Press,
517                position: ClientMousePosition {
518                    row: 0,
519                    col: config.tab_bar.width.saturating_add(1),
520                },
521            };
522
523            assert_that!(
524                handle_mouse_input_action(&config, event, &input_sender, &mut renderer).await?,
525                eq(ClientInputSend::Accepted)
526            );
527
528            assert_that!(
529                input_receiver.recv().await,
530                eq(Some(ClientRequest::Mouse(ClientMouseEvent {
531                    position: ClientMousePosition { row: 0, col: 1 },
532                    ..event
533                })))
534            );
535            Ok(())
536        })
537    }
538
539    #[test]
540    fn test_handle_mouse_input_action_when_pane_wheel_request_queue_is_full_waits_for_queue_space()
541    -> rootcause::Result<()> {
542        self::runtime()?.block_on(async {
543            let config = MuxrConfig::default();
544            let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
545            assert_that!(input_sender.try_send(ClientRequest::Pong), ok(eq(())));
546            let mut renderer = ClientRenderer::with_synchronized_output(
547                self::layout_snapshot()?,
548                self::pane_regions_snapshot()?,
549                SynchronizedOutput::Csi,
550            );
551            let event = ClientMouseEvent {
552                button: 64,
553                phase: ClientMouseEventPhase::Press,
554                position: ClientMousePosition {
555                    row: 0,
556                    col: config.tab_bar.width.saturating_add(1),
557                },
558            };
559            let handle = handle_mouse_input_action(&config, event, &input_sender, &mut renderer);
560            tokio::pin!(handle);
561
562            tokio::select! {
563                result = &mut handle => {
564                    return Err(report!("muxr wheel request did not wait for queue space").attach(format!("{result:?}")));
565                }
566                () = tokio::time::sleep(std::time::Duration::from_millis(50)) => {}
567            }
568
569            assert_that!(input_receiver.recv().await, eq(Some(ClientRequest::Pong)));
570            assert_that!(handle.await?, eq(ClientInputSend::Accepted));
571            assert_that!(input_receiver.recv().await, eq(Some(ClientRequest::Mouse(ClientMouseEvent {
572                    position: ClientMousePosition { row: 0, col: 1 },
573                    ..event
574                }))));
575            Ok(())
576        })
577    }
578
579    #[test]
580    fn test_handle_mouse_input_action_when_tracking_drag_crosses_pane_routes_to_pressed_pane() -> rootcause::Result<()>
581    {
582        self::runtime()?.block_on(async {
583            let config = MuxrConfig::default();
584            let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(4);
585            let mut renderer = ClientRenderer::with_synchronized_output(
586                self::layout_snapshot()?,
587                self::split_mouse_tracking_pane_regions_snapshot()?,
588                SynchronizedOutput::Csi,
589            );
590            let events = [
591                ClientMouseEvent {
592                    button: 0,
593                    phase: ClientMouseEventPhase::Press,
594                    position: ClientMousePosition {
595                        row: 0,
596                        col: config.tab_bar.width.saturating_add(1),
597                    },
598                },
599                ClientMouseEvent {
600                    button: 32,
601                    phase: ClientMouseEventPhase::Press,
602                    position: ClientMousePosition {
603                        row: 0,
604                        col: config.tab_bar.width.saturating_add(3),
605                    },
606                },
607                ClientMouseEvent {
608                    button: 0,
609                    phase: ClientMouseEventPhase::Release,
610                    position: ClientMousePosition { row: 0, col: 1 },
611                },
612                ClientMouseEvent {
613                    button: 32,
614                    phase: ClientMouseEventPhase::Press,
615                    position: ClientMousePosition {
616                        row: 0,
617                        col: config.tab_bar.width.saturating_add(3),
618                    },
619                },
620            ];
621            for event in events {
622                assert_that!(
623                    handle_mouse_input_action(&config, event, &input_sender, &mut renderer).await?,
624                    eq(ClientInputSend::Accepted)
625                );
626            }
627
628            assert_that!(
629                self::recv_client_request(&mut input_receiver).await?,
630                eq(Some(ClientRequest::Mouse(ClientMouseEvent {
631                    button: 0,
632                    phase: ClientMouseEventPhase::Press,
633                    position: ClientMousePosition { row: 0, col: 1 }
634                })))
635            );
636            assert_that!(
637                self::recv_client_request(&mut input_receiver).await?,
638                eq(Some(ClientRequest::Mouse(ClientMouseEvent {
639                    button: 32,
640                    phase: ClientMouseEventPhase::Press,
641                    position: ClientMousePosition { row: 0, col: 1 }
642                })))
643            );
644            assert_that!(
645                self::recv_client_request(&mut input_receiver).await?,
646                eq(Some(ClientRequest::Mouse(ClientMouseEvent {
647                    button: 0,
648                    phase: ClientMouseEventPhase::Release,
649                    position: ClientMousePosition { row: 0, col: 0 }
650                })))
651            );
652            assert_that!(
653                input_receiver.try_recv(),
654                err(matches_pattern!(tokio::sync::mpsc::error::TryRecvError::Empty))
655            );
656            Ok(())
657        })
658    }
659
660    async fn recv_client_request(
661        input_receiver: &mut tokio::sync::mpsc::Receiver<ClientRequest>,
662    ) -> rootcause::Result<Option<ClientRequest>> {
663        Ok(
664            tokio::time::timeout(std::time::Duration::from_secs(1), input_receiver.recv())
665                .await
666                .context("timed out waiting for muxr client request")?,
667        )
668    }
669
670    fn layout_snapshot() -> rootcause::Result<LayoutSnapshot> {
671        let active_tab = TabId::new(1)?;
672        let active_pane = PaneId::new(1)?;
673        let pane = PaneSnapshot {
674            tracked_process_state: muxr_core::TrackedProcessState::None,
675            cwd: "/tmp".to_owned(),
676            cmd_label: None,
677            focus_seq: 1,
678            id: active_pane,
679            title: "shell".to_owned(),
680        };
681        let tab = TabSnapshot::new(active_tab, "default", active_pane, vec![pane])?;
682        LayoutSnapshot::new(active_tab, vec![tab])
683    }
684
685    fn pane_regions_snapshot() -> rootcause::Result<PaneRegionsSnapshot> {
686        PaneRegionsSnapshot::new(vec![muxr_core::PaneRegionSnapshot::new(
687            muxr_core::PaneId::new(1)?,
688            0,
689            0,
690            2,
691            1,
692            muxr_core::PaneMouseMode::None,
693            0,
694        )?])
695    }
696
697    fn file_link_pane_regions_snapshot() -> rootcause::Result<PaneRegionsSnapshot> {
698        PaneRegionsSnapshot::new(vec![muxr_core::PaneRegionSnapshot::new(
699            muxr_core::PaneId::new(1)?,
700            0,
701            0,
702            16,
703            1,
704            muxr_core::PaneMouseMode::PressRelease,
705            0,
706        )?])
707    }
708
709    fn two_tab_layout() -> rootcause::Result<LayoutSnapshot> {
710        LayoutSnapshot::new(
711            TabId::new(1)?,
712            vec![
713                TabSnapshot::new(
714                    TabId::new(1)?,
715                    "default",
716                    PaneId::new(1)?,
717                    vec![PaneSnapshot {
718                        tracked_process_state: muxr_core::TrackedProcessState::None,
719                        cwd: "/tmp/tab-1".to_owned(),
720                        cmd_label: None,
721                        focus_seq: 1,
722                        id: PaneId::new(1)?,
723                        title: "shell".to_owned(),
724                    }],
725                )?,
726                TabSnapshot::new(
727                    TabId::new(2)?,
728                    "tab 2",
729                    PaneId::new(2)?,
730                    vec![PaneSnapshot {
731                        tracked_process_state: muxr_core::TrackedProcessState::None,
732                        cwd: "/tmp/tab-2".to_owned(),
733                        cmd_label: None,
734                        focus_seq: 1,
735                        id: PaneId::new(2)?,
736                        title: "shell".to_owned(),
737                    }],
738                )?,
739            ],
740        )
741    }
742
743    fn mouse_tracking_pane_regions_snapshot() -> rootcause::Result<PaneRegionsSnapshot> {
744        PaneRegionsSnapshot::new(vec![muxr_core::PaneRegionSnapshot::new(
745            muxr_core::PaneId::new(1)?,
746            0,
747            0,
748            2,
749            1,
750            muxr_core::PaneMouseMode::ButtonMotion,
751            0,
752        )?])
753    }
754
755    fn split_mouse_tracking_pane_regions_snapshot() -> rootcause::Result<PaneRegionsSnapshot> {
756        PaneRegionsSnapshot::new(vec![
757            muxr_core::PaneRegionSnapshot::new(
758                muxr_core::PaneId::new(1)?,
759                0,
760                0,
761                2,
762                1,
763                muxr_core::PaneMouseMode::ButtonMotion,
764                0,
765            )?,
766            muxr_core::PaneRegionSnapshot::new(
767                muxr_core::PaneId::new(2)?,
768                2,
769                0,
770                2,
771                1,
772                muxr_core::PaneMouseMode::None,
773                0,
774            )?,
775        ])
776    }
777
778    fn render_baseline() -> rootcause::Result<muxr_core::RenderBaseline> {
779        muxr_core::RenderBaseline::new(
780            1,
781            TerminalSize::new(2, 1)?,
782            muxr_core::RenderCursor {
783                row: 0,
784                col: 1,
785                shape: muxr_core::RenderCursorShape::Default,
786                visibility: muxr_core::RenderCursorVisibility::Visible,
787            },
788            vec![muxr_core::RenderRowSpan::new(
789                0,
790                0,
791                vec![self::render_cell("a"), self::render_cell("b")],
792            )?],
793        )
794    }
795
796    fn file_link_render_baseline() -> rootcause::Result<muxr_core::RenderBaseline> {
797        let text = "/tmp/foo.rs:42:7";
798        muxr_core::RenderBaseline::new(
799            1,
800            TerminalSize::new(16, 1)?,
801            muxr_core::RenderCursor {
802                row: 0,
803                col: 0,
804                shape: muxr_core::RenderCursorShape::Default,
805                visibility: muxr_core::RenderCursorVisibility::Visible,
806            },
807            vec![muxr_core::RenderRowSpan::new(
808                0,
809                0,
810                text.chars().map(|ch| render_cell(&ch.to_string())).collect(),
811            )?],
812        )
813    }
814
815    fn render_cell(text: &str) -> muxr_core::RenderCell {
816        muxr_core::RenderCell::narrow(text, muxr_core::RenderStyle::default())
817    }
818
819    fn runtime() -> rootcause::Result<tokio::runtime::Runtime> {
820        Ok(tokio::runtime::Runtime::new().context("failed to build muxr client mouse test runtime")?)
821    }
822}