rho-coding-agent 0.24.1

A lightweight agent harness inspired by Pi
Documentation
use std::time::Instant;

use crossterm::event::{MouseButton, MouseEventKind};
use ratatui::{backend::Backend, layout::Rect, Terminal};

use super::{
    copy_interaction::{code_block_copy_target_at, selection_position, selection_position_clamped},
    text_selection::{CopyNotice, TextSelection},
    App,
};

impl App {
    pub(super) fn handle_mouse_event<B: Backend>(
        &mut self,
        kind: MouseEventKind,
        column: u16,
        row: u16,
        terminal: &mut Terminal<B>,
    ) -> Result<(), B::Error> {
        let size = terminal.size()?;
        let width = size.width as usize;
        let height = size.height as usize;
        let now = Instant::now();
        match kind {
            MouseEventKind::ScrollUp => {
                self.text_selection = None;
                self.hovered_code_block_copy = None;
                self.reveal_history_scrollbar(now);
                self.history_scrollbar_drag = None;
                self.scroll_history_lines(width, height, now, -3);
            }
            MouseEventKind::ScrollDown => {
                self.text_selection = None;
                self.hovered_code_block_copy = None;
                self.reveal_history_scrollbar(now);
                self.history_scrollbar_drag = None;
                self.scroll_history_lines(width, height, now, 3);
            }
            MouseEventKind::Down(MouseButton::Left) => {
                let layout = self.screen_layout(Rect::new(0, 0, size.width, size.height), now);
                let history_len = self.history_len(width, now);
                let history_start =
                    self.visible_history_start(history_len, layout.history.height as usize);
                let targets = self.code_block_copy_targets(width);
                let code_target =
                    code_block_copy_target_at(&targets, layout.history, history_start, column, row);
                let scrollbar = layout
                    .history_scrollbar
                    .filter(|scrollbar| scrollbar.contains(column, row))
                    .filter(|_| self.should_render_history_scrollbar(now));
                self.update_history_scrollbar_hover(layout.history_scrollbar, column, row);
                self.hovered_code_block_copy = code_target.as_ref().map(|target| target.line);
                if let Some(scrollbar) = scrollbar {
                    self.text_selection = None;
                    self.reveal_history_scrollbar(now);
                    let drag = scrollbar.begin_drag(row);
                    self.history_scrollbar_drag = Some(drag);
                    self.history_scroll = scrollbar.scroll_state_for_pointer(row, drag);
                } else if layout.jump_to_bottom.is_some_and(|rect| {
                    rect.contains(ratatui::layout::Position { x: column, y: row })
                }) {
                    self.text_selection = None;
                    self.history_scrollbar_drag = None;
                    self.scroll_history_to_bottom();
                } else if let Some(target) = code_target {
                    self.text_selection = None;
                    self.copy_text(&target.text, now);
                } else if let Some(position) =
                    selection_position(layout.history, history_start, column, row)
                {
                    self.history_scrollbar_drag = None;
                    self.text_selection = Some(TextSelection::new(position));
                } else {
                    self.text_selection = None;
                }
            }
            MouseEventKind::Drag(MouseButton::Left) => {
                let layout = self.screen_layout(Rect::new(0, 0, size.width, size.height), now);
                self.update_history_scrollbar_hover(layout.history_scrollbar, column, row);
                if let Some(drag) = self.history_scrollbar_drag {
                    self.text_selection = None;
                    self.hovered_code_block_copy = None;
                    if let Some(scrollbar) = layout.history_scrollbar {
                        self.history_scroll = scrollbar.scroll_state_for_pointer(row, drag);
                    }
                } else {
                    let history_len = self.history_len(width, now);
                    let history_start =
                        self.visible_history_start(history_len, layout.history.height as usize);
                    let targets = self.code_block_copy_targets(width);
                    self.hovered_code_block_copy = code_block_copy_target_at(
                        &targets,
                        layout.history,
                        history_start,
                        column,
                        row,
                    )
                    .map(|target| target.line);
                    if let (Some(selection), Some(position)) = (
                        &mut self.text_selection,
                        selection_position_clamped(layout.history, history_start, column, row),
                    ) {
                        selection.update(position);
                    }
                }
            }
            MouseEventKind::Up(MouseButton::Left) => {
                let was_scrollbar_drag = self.history_scrollbar_drag.take().is_some();
                let layout = self.screen_layout(Rect::new(0, 0, size.width, size.height), now);
                self.update_history_scrollbar_hover(layout.history_scrollbar, column, row);
                let history_len = self.history_len(width, now);
                let history_start =
                    self.visible_history_start(history_len, layout.history.height as usize);
                let targets = self.code_block_copy_targets(width);
                self.hovered_code_block_copy =
                    code_block_copy_target_at(&targets, layout.history, history_start, column, row)
                        .map(|target| target.line);
                if was_scrollbar_drag {
                    self.text_selection = None;
                } else if let Some(mut selection) = self.text_selection.take() {
                    if let Some(position) =
                        selection_position_clamped(layout.history, history_start, column, row)
                    {
                        selection.update(position);
                    }
                    if selection.has_moved() {
                        let visible_lines = self.visible_history_lines(
                            width,
                            now,
                            history_start,
                            layout.history.height as usize,
                        );
                        if let Some(text) = selection.selected_text(&visible_lines, history_start) {
                            self.copy_text(&text, now);
                            self.text_selection = Some(selection);
                        }
                    }
                }
            }
            MouseEventKind::Moved => {
                let layout = self.screen_layout(Rect::new(0, 0, size.width, size.height), now);
                self.update_history_scrollbar_hover(layout.history_scrollbar, column, row);
                let history_len = self.history_len(width, now);
                let history_start =
                    self.visible_history_start(history_len, layout.history.height as usize);
                let targets = self.code_block_copy_targets(width);
                self.hovered_code_block_copy =
                    code_block_copy_target_at(&targets, layout.history, history_start, column, row)
                        .map(|target| target.line);
            }
            MouseEventKind::Down(MouseButton::Right)
            | MouseEventKind::Down(MouseButton::Middle)
            | MouseEventKind::Up(MouseButton::Right)
            | MouseEventKind::Up(MouseButton::Middle)
            | MouseEventKind::Drag(MouseButton::Right)
            | MouseEventKind::Drag(MouseButton::Middle)
            | MouseEventKind::ScrollLeft
            | MouseEventKind::ScrollRight => {}
        }
        Ok(())
    }

    fn copy_text(&mut self, text: &str, now: Instant) {
        self.copy_notice = Some(match self.clipboard.copy(text) {
            Ok(()) => CopyNotice::copied(text.chars().count(), now),
            Err(error) => CopyNotice::failed(&error, now),
        });
    }
}