diff --git a/src/app.rs b/src/app.rs index e0c49c4..8cdec92 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::config::{AppliesTo, Config, CustomMenuItem}; +use crate::config::{AppliesTo, Config}; use crate::file_ops::{self, FileEntry}; #[derive(Debug, Clone, PartialEq)] @@ -107,6 +107,11 @@ pub struct App { pub settings_hover_button: Option, // Terminal capabilities 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 @@ -181,6 +186,7 @@ impl App { settings_custom_selected: 0, settings_hover_button: None, 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) -> ContextMenu { let has_selection = !self.selected_indices.is_empty(); 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 let selected_is_dir = self.selected_indices.iter().all(|&i| { diff --git a/src/events.rs b/src/events.rs index 69b8a9c..a25aa57 100644 --- a/src/events.rs +++ b/src/events.rs @@ -32,6 +32,10 @@ fn handle_event(app: &mut App, ev: Event) -> bool { handle_mouse(app, mouse); false } + Event::Resize(w, h) => { + app.term_size = (w, h); + 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) let ns_idx = m.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu)); 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_y = my + 1 + ns_pos as u16; 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 settings is already open and we click inside the dialog – handled by settings_click 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); } else { 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 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); 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 if app.dialog == DialogMode::Settings { 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) && 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 if let Some(ns_pos) = menu.items.iter().position(|i| matches!(i, ContextMenuItem::NewSubmenu)) { 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_y = my + 1 + ns_pos as u16; diff --git a/src/ui.rs b/src/ui.rs index 39d5f5b..271243c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -56,9 +56,11 @@ pub struct LayoutAreas { pub path_input: Rect, } -/// Compute the layout areas for hit testing (without a frame) -pub fn compute_layout(_app: &App) -> LayoutAreas { - let (width, height) = crossterm::terminal::size().unwrap_or((120, 40)); +/// Compute the layout areas for hit testing (without a frame). +/// Uses the last size seen by draw() instead of querying the terminal, +/// 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); split_layout(full) } @@ -97,6 +99,7 @@ fn split_layout(area: Rect) -> LayoutAreas { pub fn draw(f: &mut Frame, app: &mut App) { let area = f.area(); + app.term_size = (area.width, area.height); let layout = split_layout(area); 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. -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_height: u16 = 26; 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); Rect::new(x, y, dlg_width, dlg_height) } 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 area = Rect::new( area.x.min(term_area.width.saturating_sub(area.width)),