From 07c9a01b68c7e62c00231339af0915e9122e687b Mon Sep 17 00:00:00 2001 From: wismyzhizi2018 Date: Sat, 27 Jun 2026 20:47:04 +0800 Subject: [PATCH] feat(tui): improve text selection auto-scroll and copy toast UX - Move copy success toast from status bar to floating overlay at bottom-right of message area (aligned with Claude Code) - Add auto-scroll when text selection drag exceeds viewport boundary - Auto-scroll speed scales with distance from viewport edge (1-5 lines per event) - Update copy toast i18n text for better readability Co-Authored-By: mimo-v2.5-pro --- peri-tui/locales/en/main.ftl | 2 +- peri-tui/src/event/mod.rs | 31 +++++++++++++++++------ peri-tui/src/ui/main_ui/mod.rs | 36 +++++++++++++++++++++++++++ peri-tui/src/ui/main_ui/status_bar.rs | 21 +--------------- 4 files changed, 62 insertions(+), 28 deletions(-) diff --git a/peri-tui/locales/en/main.ftl b/peri-tui/locales/en/main.ftl index d6dc7cf1..f6d01cb8 100644 --- a/peri-tui/locales/en/main.ftl +++ b/peri-tui/locales/en/main.ftl @@ -96,7 +96,7 @@ statusbar-permission-accept-edit = Accept Edit statusbar-permission-auto = Auto Mode statusbar-permission-bypass = Bypass statusbar-permission-cycle-hint = (Shift+Tab to cycle) -statusbar-copied = { $count } chars copied +statusbar-copied = Copied { $count } chars to clipboard statusbar-no-agent = None statusbar-bg-indicator = [BG: { $count }] statusbar-retrying = Retry { $attempt }/{ $max } ({ $delay }s): { $error } diff --git a/peri-tui/src/event/mod.rs b/peri-tui/src/event/mod.rs index a1cebe24..bc79d1f7 100644 --- a/peri-tui/src/event/mod.rs +++ b/peri-tui/src/event/mod.rs @@ -718,14 +718,31 @@ async fn handle_event(app: &mut App, ev: Event) -> Result> { } if app.session_mgr.current_mut().ui.text_selection.dragging { if let Some(area) = app.session_mgr.current_mut().ui.messages_area { - let visual_row = usize::from(mouse.row.saturating_sub(area.y)) - + app.session_mgr.current_mut().ui.scroll_offset; + // Auto-scroll when dragging past viewport boundary + // Speed scales with distance from viewport edge (1-5 lines per event) + let ui = &mut app.session_mgr.current_mut().ui; + let area_bottom = area.y + area.height; + if mouse.row >= area_bottom { + let distance = (mouse.row - area_bottom) as u32; + let step = (1 + distance / 2).min(5) as usize; + let max_scroll = ui.scrollbar_max_offset; + let min_scroll = ui.scrollbar_min_offset.min(max_scroll); + let next = ui.scroll_offset.saturating_add(step).min(max_scroll); + ui.scroll_offset = next.max(min_scroll); + ui.scroll_follow = next >= max_scroll; + } else if mouse.row < area.y { + let distance = (area.y - mouse.row) as u32; + let step = (1 + distance / 2).min(5) as usize; + let max_scroll = ui.scrollbar_max_offset; + let min_scroll = ui.scrollbar_min_offset.min(max_scroll); + ui.scroll_offset = ui.scroll_offset.saturating_sub(step).max(min_scroll); + ui.scroll_follow = false; + } + let scroll_offset = ui.scroll_offset; + let visual_row = + usize::from(mouse.row.saturating_sub(area.y)) + scroll_offset; let visual_col = mouse.column.saturating_sub(area.x); - app.session_mgr - .current_mut() - .ui - .text_selection - .update_drag(visual_row, visual_col); + ui.text_selection.update_drag(visual_row, visual_col); } } // Textarea area: extend textarea selection diff --git a/peri-tui/src/ui/main_ui/mod.rs b/peri-tui/src/ui/main_ui/mod.rs index 2750ed5c..f2a64391 100644 --- a/peri-tui/src/ui/main_ui/mod.rs +++ b/peri-tui/src/ui/main_ui/mod.rs @@ -106,6 +106,42 @@ fn render_session_column(f: &mut Frame, app: &mut App, area: Rect) { .split(area); message_area::render_messages(f, app, chunks[0], chunks[1]); + + // 复制成功浮动提示:消息区右下角,紧贴输入框上方(对齐 Claude Code) + if let Some(until) = app.session_mgr.current().ui.copy_message_until { + if std::time::Instant::now() < until { + let lc = &app.services.lc; + let count = app.session_mgr.current().ui.copy_char_count; + let text = format!( + " {} ", + lc.tr_args("statusbar-copied", &[("count".into(), (count as i64).into())]) + ); + let text_width = unicode_width::UnicodeWidthStr::width(text.as_str()); + let msg_area = chunks[1]; + if msg_area.height > 0 && text_width > 0 { + // scrollbar 占据最右 1 列,toast 需左移避让 + let has_scrollbar = { + let cache = app.session_mgr.current().messages.render_cache.read(); + cache.total_lines > msg_area.height as usize && msg_area.width > 1 + }; + let scrollbar_col: u16 = if has_scrollbar { 1 } else { 0 }; + let y = msg_area.y + msg_area.height - 1; + let avail_width = msg_area.width.saturating_sub(scrollbar_col); + let x = (msg_area.x + avail_width).saturating_sub(text_width as u16); + let label_area = Rect { + x, + y, + width: text_width.min(avail_width as usize) as u16, + height: 1, + }; + f.render_widget( + Paragraph::new(text).style(Style::default().fg(Color::Rgb(182, 186, 233))), + label_area, + ); + } + } + } + attachment::render_attachment_bar(f, app, chunks[2]); // 底部展开区 diff --git a/peri-tui/src/ui/main_ui/status_bar.rs b/peri-tui/src/ui/main_ui/status_bar.rs index 63be86c5..a3880ebc 100644 --- a/peri-tui/src/ui/main_ui/status_bar.rs +++ b/peri-tui/src/ui/main_ui/status_bar.rs @@ -268,26 +268,7 @@ fn render_third_row(f: &mut Frame, app: &App, area: Rect) { left_spans.push(Span::styled(hint, Style::default().fg(theme::DIM))); } - // 瞬时状态 - // 复制成功提示 - if let Some(until) = app.session_mgr.current().ui.copy_message_until { - if std::time::Instant::now() < until { - if has_content { - left_spans.push(Span::styled(" · ", Style::default().fg(theme::MUTED))); - } - let count = app.session_mgr.current().ui.copy_char_count; - left_spans.push(Span::styled( - format!( - " {}", - lc.tr_args( - "statusbar-copied", - &[("count".into(), (count as i64).into()),] - ) - ), - Style::default().fg(theme::MUTED), - )); - } - } + // 瞬时状态(复制提示已移至消息区右下角浮动显示) // 后台任务指示器 if !app.session_mgr.current().background_agents.is_empty() {