Fix nested-terminal behavior: track size from frame/resize events

- Add App::term_size, updated from the drawn frame area and
  Event::Resize instead of polling crossterm::terminal::size() in
  hit-testing and dialog positioning (correct inside TUI-WM/tmux)
- Handle Event::Resize explicitly
- Fix can_paste tautology (|| true)
- Drop unused import

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:42:19 +02:00
parent ec61b057ac
commit eba6e96743
3 changed files with 27 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::config::{AppliesTo, Config, CustomMenuItem}; use crate::config::{AppliesTo, Config};
use crate::file_ops::{self, FileEntry}; use crate::file_ops::{self, FileEntry};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@@ -107,6 +107,11 @@ pub struct App {
pub settings_hover_button: Option<u8>, pub settings_hover_button: Option<u8>,
// Terminal capabilities // Terminal capabilities
pub unicode_support: bool, pub unicode_support: bool,
/// Senast kända terminalstorlek (kolumner, rader). Uppdateras från
/// frame-arean vid varje draw och från Event::Resize — pollar aldrig
/// terminalen, vilket gör hit-testing korrekt även i nästlade
/// terminaler (t.ex. inne i TUI-WM).
pub term_size: (u16, u16),
} }
// Which field inside the settings editor is being edited // Which field inside the settings editor is being edited
@@ -181,6 +186,7 @@ impl App {
settings_custom_selected: 0, settings_custom_selected: 0,
settings_hover_button: None, settings_hover_button: None,
unicode_support: detect_unicode_support(), unicode_support: detect_unicode_support(),
term_size: crossterm::terminal::size().unwrap_or((80, 24)),
} }
} }
@@ -423,7 +429,9 @@ impl App {
pub fn build_context_menu(&self, x: u16, y: u16, is_sidebar_fav: bool, fav_path: Option<String>) -> ContextMenu { pub fn build_context_menu(&self, x: u16, y: u16, is_sidebar_fav: bool, fav_path: Option<String>) -> ContextMenu {
let has_selection = !self.selected_indices.is_empty(); let has_selection = !self.selected_indices.is_empty();
let has_clipboard = self.clipboard.is_some(); let has_clipboard = self.clipboard.is_some();
let can_paste = has_clipboard && (self.selected_target_dir().is_some() || true); // Inklistring går till markerad katalog om en sådan finns, annars
// aktuell katalog — så clipboard-innehåll räcker.
let can_paste = has_clipboard;
// Determine if selected items are files/dirs for custom item filtering // Determine if selected items are files/dirs for custom item filtering
let selected_is_dir = self.selected_indices.iter().all(|&i| { let selected_is_dir = self.selected_indices.iter().all(|&i| {

View File

@@ -32,6 +32,10 @@ fn handle_event(app: &mut App, ev: Event) -> bool {
handle_mouse(app, mouse); handle_mouse(app, mouse);
false false
} }
Event::Resize(w, h) => {
app.term_size = (w, h);
false
}
_ => false, _ => false,
} }
} }
@@ -744,7 +748,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) {
// Check if click lands in the submenu (only visible when NewSubmenu is highlighted) // Check if click lands in the submenu (only visible when NewSubmenu is highlighted)
let ns_idx = m.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu)); let ns_idx = m.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu));
let in_sub = if let Some(ns_pos) = ns_idx { let in_sub = if let Some(ns_pos) = ns_idx {
let (term_w, _) = crossterm::terminal::size().unwrap_or((120, 40)); let (term_w, _) = app.term_size;
let sub_x = (mx + MENU_W).min(term_w.saturating_sub(SUB_W)); let sub_x = (mx + MENU_W).min(term_w.saturating_sub(SUB_W));
let sub_y = my + 1 + ns_pos as u16; let sub_y = my + 1 + ns_pos as u16;
col >= sub_x && col < sub_x + SUB_W col >= sub_x && col < sub_x + SUB_W
@@ -800,7 +804,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) {
if row == gear_row && col >= gear_col && col < gear_col + 3 { if row == gear_row && col >= gear_col && col < gear_col + 3 {
// If settings is already open and we click inside the dialog handled by settings_click // If settings is already open and we click inside the dialog handled by settings_click
if app.dialog == DialogMode::Settings { if app.dialog == DialogMode::Settings {
let dlg = ui::settings_dialog_rect(sb.x + sb.width); let dlg = ui::settings_dialog_rect(sb.x + sb.width, app.term_size.1);
settings_click(app, col, row, dlg); settings_click(app, col, row, dlg);
} else { } else {
app.dialog = DialogMode::Settings; app.dialog = DialogMode::Settings;
@@ -810,7 +814,7 @@ fn handle_left_click(app: &mut App, col: u16, row: u16) {
// If settings dialog is open, route all clicks into it // If settings dialog is open, route all clicks into it
if app.dialog == DialogMode::Settings { if app.dialog == DialogMode::Settings {
let dlg = ui::settings_dialog_rect(sb.x + sb.width); let dlg = ui::settings_dialog_rect(sb.x + sb.width, app.term_size.1);
settings_click(app, col, row, dlg); settings_click(app, col, row, dlg);
return; return;
} }
@@ -920,7 +924,7 @@ fn handle_mouse_move(app: &mut App, col: u16, row: u16) {
// Settings hover: highlight rows when mouse moves over the dialog // Settings hover: highlight rows when mouse moves over the dialog
if app.dialog == DialogMode::Settings { if app.dialog == DialogMode::Settings {
let layout = ui::compute_layout(app); let layout = ui::compute_layout(app);
let dlg = ui::settings_dialog_rect(layout.status_bar.x + layout.status_bar.width); let dlg = ui::settings_dialog_rect(layout.status_bar.x + layout.status_bar.width, app.term_size.1);
if col > dlg.x && col < dlg.x + dlg.width.saturating_sub(1) if col > dlg.x && col < dlg.x + dlg.width.saturating_sub(1)
&& row > dlg.y && row < dlg.y + dlg.height.saturating_sub(1) && row > dlg.y && row < dlg.y + dlg.height.saturating_sub(1)
{ {
@@ -983,7 +987,7 @@ fn handle_mouse_move(app: &mut App, col: u16, row: u16) {
// When NewSubmenu is highlighted, check if the mouse moves into the flyout // When NewSubmenu is highlighted, check if the mouse moves into the flyout
if let Some(ns_pos) = menu.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu)) { if let Some(ns_pos) = menu.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu)) {
if menu.selected == ns_pos { if menu.selected == ns_pos {
let (term_w, _) = crossterm::terminal::size().unwrap_or((120, 40)); let (term_w, _) = app.term_size;
let sub_x = (mx + MENU_W).min(term_w.saturating_sub(SUB_W)); let sub_x = (mx + MENU_W).min(term_w.saturating_sub(SUB_W));
let sub_y = my + 1 + ns_pos as u16; let sub_y = my + 1 + ns_pos as u16;

View File

@@ -56,9 +56,11 @@ pub struct LayoutAreas {
pub path_input: Rect, pub path_input: Rect,
} }
/// Compute the layout areas for hit testing (without a frame) /// Compute the layout areas for hit testing (without a frame).
pub fn compute_layout(_app: &App) -> LayoutAreas { /// Uses the last size seen by draw() instead of querying the terminal,
let (width, height) = crossterm::terminal::size().unwrap_or((120, 40)); /// so it stays correct inside nested terminals.
pub fn compute_layout(app: &App) -> LayoutAreas {
let (width, height) = app.term_size;
let full = Rect::new(0, 0, width, height); let full = Rect::new(0, 0, width, height);
split_layout(full) split_layout(full)
} }
@@ -97,6 +99,7 @@ fn split_layout(area: Rect) -> LayoutAreas {
pub fn draw(f: &mut Frame, app: &mut App) { pub fn draw(f: &mut Frame, app: &mut App) {
let area = f.area(); let area = f.area();
app.term_size = (area.width, area.height);
let layout = split_layout(area); let layout = split_layout(area);
draw_sidebar(f, app, layout.sidebar); draw_sidebar(f, app, layout.sidebar);
@@ -644,17 +647,16 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) {
} }
/// Compute the settings dialog rect anchored to the bottom-right corner. /// Compute the settings dialog rect anchored to the bottom-right corner.
pub fn settings_dialog_rect(right_edge: u16) -> ratatui::layout::Rect { pub fn settings_dialog_rect(right_edge: u16, term_h: u16) -> ratatui::layout::Rect {
let dlg_width: u16 = 72; let dlg_width: u16 = 72;
let dlg_height: u16 = 26; let dlg_height: u16 = 26;
let x = right_edge.saturating_sub(dlg_width + 1); let x = right_edge.saturating_sub(dlg_width + 1);
let (_, term_h) = crossterm::terminal::size().unwrap_or((120, 40));
let y = term_h.saturating_sub(dlg_height + 3); let y = term_h.saturating_sub(dlg_height + 3);
Rect::new(x, y, dlg_width, dlg_height) Rect::new(x, y, dlg_width, dlg_height)
} }
fn draw_settings(f: &mut Frame, app: &App, right_edge: u16) { fn draw_settings(f: &mut Frame, app: &App, right_edge: u16) {
let area = settings_dialog_rect(right_edge); let area = settings_dialog_rect(right_edge, f.area().height);
let term_area = f.area(); let term_area = f.area();
let area = Rect::new( let area = Rect::new(
area.x.min(term_area.width.saturating_sub(area.width)), area.x.min(term_area.width.saturating_sub(area.width)),