diff --git a/crates/nyaterm-desktop/src/features/pages/remote/docker/containers.rs b/crates/nyaterm-desktop/src/features/pages/remote/docker/containers.rs index 5a54b165..687a873c 100644 --- a/crates/nyaterm-desktop/src/features/pages/remote/docker/containers.rs +++ b/crates/nyaterm-desktop/src/features/pages/remote/docker/containers.rs @@ -6,7 +6,7 @@ use nyaterm_core::truncate_preview; use nyaterm_transport::DockerContainer; use super::super::panels::RemoteMonitorPanel; -use crate::features::remote::DOCKER_VIEWPORT_ROWS; +use crate::features::remote::{DOCKER_VIEWPORT_ROWS, state_scrolled_list_range}; use crate::features::{ formatting::compact_id, formatting::docker_state_color, formatting::docker_state_rank, shell::gpui_code_font_family, @@ -96,27 +96,16 @@ pub(in crate::features::pages::remote) fn docker_containers_panel( .then(left.name.cmp(&right.name)) }); - // Tauri-like virtual list: fixed row slot, overscan window, spacer padding + wheel. + // Wheel-driven row window. The offset replaces the rendered slice; this container + // does not have a native scroll position that could consume spacer padding. const DOCKER_ROW_PX: f32 = 66.; const DOCKER_OVERSCAN: usize = 6; let total = containers.len(); - let window_capacity = DOCKER_VIEWPORT_ROWS + DOCKER_OVERSCAN * 2; - let max_offset = total.saturating_sub(DOCKER_VIEWPORT_ROWS.min(total)); - let scroll_row = list_offset.min(max_offset); - let window_start = scroll_row.saturating_sub(DOCKER_OVERSCAN); - let window_end = (window_start + window_capacity).min(total); - let visible = containers - .get(window_start..window_end) - .unwrap_or(&[]) - .to_vec(); - let pad_top = (window_start as f32) * DOCKER_ROW_PX; - let pad_bottom = ((total.saturating_sub(window_end)) as f32) * DOCKER_ROW_PX; + let visible_range = + state_scrolled_list_range(total, list_offset, DOCKER_VIEWPORT_ROWS, DOCKER_OVERSCAN); let mut rows = div().flex().flex_col().gap(px(6.)); - if pad_top > 0. { - rows = rows.child(div().h(px(pad_top)).w_full().flex_none()); - } - for container in visible { + for container in containers.get(visible_range).unwrap_or(&[]).iter().cloned() { let menu_open = open_menu_id == Some(container.id.as_str()); rows = rows.child(docker_container_row( context.clone(), @@ -125,9 +114,6 @@ pub(in crate::features::pages::remote) fn docker_containers_panel( cx, )); } - if pad_bottom > 0. { - rows = rows.child(div().h(px(pad_bottom)).w_full().flex_none()); - } div() .id(SharedString::from("docker-containers-scroll")) .size_full() diff --git a/crates/nyaterm-desktop/src/features/pages/remote/docker/resources.rs b/crates/nyaterm-desktop/src/features/pages/remote/docker/resources.rs index d2aa5f0c..3cc72a00 100644 --- a/crates/nyaterm-desktop/src/features/pages/remote/docker/resources.rs +++ b/crates/nyaterm-desktop/src/features/pages/remote/docker/resources.rs @@ -7,7 +7,7 @@ use nyaterm_transport::{DockerImage, DockerNetwork, DockerVolume}; use nyaterm_ui::NyaScrollable; use super::super::panels::RemoteMonitorPanel; -use crate::features::remote::DOCKER_RESOURCE_VIEWPORT_ROWS; +use crate::features::remote::{DOCKER_RESOURCE_VIEWPORT_ROWS, state_scrolled_list_range}; use crate::features::{formatting::compact_id, shell::gpui_code_font_family}; use crate::models::{DockerConfirmAction, DockerConfirmState}; use crate::theme::ThemePalette; @@ -30,18 +30,10 @@ pub(in crate::features::pages::remote) fn docker_images_panel( } let total = images.len(); - let (window_start, window_end, pad_top, pad_bottom, scroll_offset) = - docker_resource_window(total, list_offset); + let visible_range = docker_resource_window(total, list_offset); + let window_start = visible_range.start; let mut rows = div().flex().flex_col().gap(px(6.)); - if pad_top > 0. { - rows = rows.child(div().h(px(pad_top)).w_full().flex_none()); - } - for (visible_index, image) in images - .get(window_start..window_end) - .unwrap_or(&[]) - .iter() - .enumerate() - { + for (visible_index, image) in images.get(visible_range).unwrap_or(&[]).iter().enumerate() { let row_index = window_start + visible_index; let image_id = image.id.clone(); let label = docker_image_label(image); @@ -82,10 +74,7 @@ pub(in crate::features::pages::remote) fn docker_images_panel( )), ); } - if pad_bottom > 0. { - rows = rows.child(div().h(px(pad_bottom)).w_full().flex_none()); - } - docker_resource_panel(palette, "Images", total, rows, scroll_offset, cx) + docker_resource_panel(palette, "Images", total, rows, cx) } pub(in crate::features::pages::remote) fn docker_volumes_panel( @@ -100,13 +89,9 @@ pub(in crate::features::pages::remote) fn docker_volumes_panel( } let total = volumes.len(); - let (window_start, window_end, pad_top, pad_bottom, scroll_offset) = - docker_resource_window(total, list_offset); + let visible_range = docker_resource_window(total, list_offset); let mut rows = div().flex().flex_col().gap(px(6.)); - if pad_top > 0. { - rows = rows.child(div().h(px(pad_top)).w_full().flex_none()); - } - for volume in volumes.get(window_start..window_end).unwrap_or(&[]) { + for volume in volumes.get(visible_range).unwrap_or(&[]) { let volume_name = volume.name.clone(); let row_labels = labels.clone(); rows = rows.child( @@ -140,10 +125,7 @@ pub(in crate::features::pages::remote) fn docker_volumes_panel( )), ); } - if pad_bottom > 0. { - rows = rows.child(div().h(px(pad_bottom)).w_full().flex_none()); - } - docker_resource_panel(palette, "Volumes", total, rows, scroll_offset, cx) + docker_resource_panel(palette, "Volumes", total, rows, cx) } pub(in crate::features::pages::remote) fn docker_networks_panel( @@ -158,13 +140,9 @@ pub(in crate::features::pages::remote) fn docker_networks_panel( } let total = networks.len(); - let (window_start, window_end, pad_top, pad_bottom, scroll_offset) = - docker_resource_window(total, list_offset); + let visible_range = docker_resource_window(total, list_offset); let mut rows = div().flex().flex_col().gap(px(6.)); - if pad_top > 0. { - rows = rows.child(div().h(px(pad_top)).w_full().flex_none()); - } - for network in networks.get(window_start..window_end).unwrap_or(&[]) { + for network in networks.get(visible_range).unwrap_or(&[]) { let network_id = network.id.clone(); let name = network.name.clone(); let row_labels = labels.clone(); @@ -203,21 +181,16 @@ pub(in crate::features::pages::remote) fn docker_networks_panel( )), ); } - if pad_bottom > 0. { - rows = rows.child(div().h(px(pad_bottom)).w_full().flex_none()); - } - docker_resource_panel(palette, "Networks", total, rows, scroll_offset, cx) + docker_resource_panel(palette, "Networks", total, rows, cx) } -fn docker_resource_window(total: usize, list_offset: usize) -> (usize, usize, f32, f32, usize) { - let window_capacity = DOCKER_RESOURCE_VIEWPORT_ROWS + DOCKER_RESOURCE_OVERSCAN * 2; - let max_offset = total.saturating_sub(DOCKER_RESOURCE_VIEWPORT_ROWS.min(total)); - let scroll_row = list_offset.min(max_offset); - let window_start = scroll_row.saturating_sub(DOCKER_RESOURCE_OVERSCAN); - let window_end = (window_start + window_capacity).min(total); - let pad_top = (window_start as f32) * DOCKER_RESOURCE_ROW_PX; - let pad_bottom = ((total.saturating_sub(window_end)) as f32) * DOCKER_RESOURCE_ROW_PX; - (window_start, window_end, pad_top, pad_bottom, scroll_row) +fn docker_resource_window(total: usize, list_offset: usize) -> std::ops::Range { + state_scrolled_list_range( + total, + list_offset, + DOCKER_RESOURCE_VIEWPORT_ROWS, + DOCKER_RESOURCE_OVERSCAN, + ) } fn docker_resource_empty( @@ -243,7 +216,6 @@ pub(in crate::features::pages::remote) fn docker_resource_panel( title: &'static str, count: usize, rows: impl IntoElement, - _scroll_offset: usize, cx: &mut Context, ) -> gpui::AnyElement { // Tauri resource tabs: full-height virtual list + wheel offset. diff --git a/crates/nyaterm-desktop/src/features/pages/remote/process/mod.rs b/crates/nyaterm-desktop/src/features/pages/remote/process/mod.rs index c4d2abea..edf50433 100644 --- a/crates/nyaterm-desktop/src/features/pages/remote/process/mod.rs +++ b/crates/nyaterm-desktop/src/features/pages/remote/process/mod.rs @@ -5,8 +5,8 @@ mod table; // Widened past the page: `features::remote` needs the width -> mode mapping to // constrain the sort key when the panel is resized, which used to happen in render. +pub(super) use data::process_row_height_px; pub(in crate::features) use data::{ProcessDisplayMode, process_display_mode}; -pub(super) use data::{process_details_height_px, process_row_height_px}; pub(super) use details::{ProcessDetailLabels, process_details}; pub(super) use resources::usage_color; pub(super) use table::{ diff --git a/crates/nyaterm-desktop/src/features/pages/remote/process_view.rs b/crates/nyaterm-desktop/src/features/pages/remote/process_view.rs index 50b67f2d..c24cc179 100644 --- a/crates/nyaterm-desktop/src/features/pages/remote/process_view.rs +++ b/crates/nyaterm-desktop/src/features/pages/remote/process_view.rs @@ -8,7 +8,7 @@ use gpui::{ }; use nyaterm_transport::{PROCESS_LIST_UNSUPPORTED_ERROR, RemoteProcess}; -use crate::features::remote::PROCESS_VIEWPORT_ROWS; +use crate::features::remote::{PROCESS_VIEWPORT_ROWS, state_scrolled_list_range}; use std::sync::Arc; use super::panels::{PanelChrome, RemoteMonitorPanel}; @@ -21,8 +21,8 @@ use nyaterm_ui::{NyaInputState, NyaNumberInputState, NyaSearchInput}; use super::process::{ ProcessDetailLabels, ProcessDisplayMode, ProcessTableLabels, ProcessTableRowActions, - ProcessTableRowPresentation, process_details, process_details_height_px, process_display_mode, - process_row_height_px, process_sort_button, process_table_row, + ProcessTableRowPresentation, process_details, process_display_mode, process_row_height_px, + process_sort_button, process_table_row, }; /// The Processes panel, rendered from a snapshot. @@ -103,37 +103,18 @@ pub(in crate::features::pages::remote) fn processes_panel( // both when the data, the query, the sort or the panel width changes. This pass // only reads them. - // Tauri-like virtual list: base row + expanded details height, spacer padding. + // Wheel-driven row window. The scroll offset is authoritative state rather than a + // native scroll position, so the first rendered row must be the offset row itself. let process_row_px = process_row_height_px(mode); - let process_details_px = process_details_height_px(mode); const PROCESS_OVERSCAN: usize = 8; - let selected_pid = process_state.selected_pid; - let row_height = |process: &RemoteProcess| -> f32 { - if selected_pid == Some(process.pid) { - process_row_px + process_details_px - } else { - process_row_px - } - }; let total_filtered = filtered_processes.len(); - let window_capacity = PROCESS_VIEWPORT_ROWS + PROCESS_OVERSCAN * 2; - let scroll_row = process_state.list_offset; - let window_start = scroll_row.saturating_sub(PROCESS_OVERSCAN); - let window_end = (window_start + window_capacity).min(total_filtered); - let visible_processes = filtered_processes - .get(window_start..window_end) - .unwrap_or(&[]) - .to_vec(); - let pad_top = filtered_processes - .iter() - .take(window_start) - .map(row_height) - .sum::(); - let pad_bottom = filtered_processes - .iter() - .skip(window_end) - .map(row_height) - .sum::(); + let visible_range = state_scrolled_list_range( + total_filtered, + process_state.list_offset, + PROCESS_VIEWPORT_ROWS, + PROCESS_OVERSCAN, + ); + let visible_processes = filtered_processes.get(visible_range).unwrap_or(&[]); let selected_process = process_state .selected_pid @@ -167,10 +148,7 @@ pub(in crate::features::pages::remote) fn processes_panel( "icons/processes.svg", )); } else { - if pad_top > 0. { - rows = rows.child(div().h(px(pad_top)).w_full().flex_none()); - } - for process in visible_processes.iter() { + for process in visible_processes { let pid = process.pid; let selected = process_state.selected_pid == Some(pid); rows = rows.child( @@ -282,9 +260,6 @@ pub(in crate::features::pages::remote) fn processes_panel( ), ); } - if pad_bottom > 0. { - rows = rows.child(div().h(px(pad_bottom)).w_full().flex_none()); - } } // Tauri ProcessManager shell: dense search toolbar + sort strip + scrollable table. diff --git a/crates/nyaterm-desktop/src/features/remote/list_window.rs b/crates/nyaterm-desktop/src/features/remote/list_window.rs index f35317ee..8752358a 100644 --- a/crates/nyaterm-desktop/src/features/remote/list_window.rs +++ b/crates/nyaterm-desktop/src/features/remote/list_window.rs @@ -31,9 +31,29 @@ pub(in crate::features) fn max_list_offset(total: usize, viewport_rows: usize) - total.saturating_sub(viewport_rows.min(total)) } +/// The rows rendered by a wheel-driven list that owns its offset as state. +/// +/// These lists do not move a native scroll container. Changing `offset` replaces the +/// rendered rows, so the window must begin at the offset itself. Leading spacer rows +/// would be visible content and create an increasingly large blank area while scrolling. +/// Overscan is therefore added only after the visible viewport. +pub(in crate::features) fn state_scrolled_list_range( + total: usize, + offset: usize, + viewport_rows: usize, + overscan_rows: usize, +) -> std::ops::Range { + let start = offset.min(max_list_offset(total, viewport_rows)); + let end = start + .saturating_add(viewport_rows) + .saturating_add(overscan_rows) + .min(total); + start..end +} + #[cfg(test)] mod tests { - use super::max_list_offset; + use super::{max_list_offset, state_scrolled_list_range}; #[test] fn a_list_shorter_than_the_viewport_pins_to_the_top() { @@ -47,4 +67,15 @@ mod tests { assert_eq!(max_list_offset(17, 16), 1); assert_eq!(max_list_offset(100, 16), 84); } + + #[test] + fn a_state_scrolled_window_starts_at_the_requested_offset() { + assert_eq!(state_scrolled_list_range(100, 20, 16, 6), 20..42); + } + + #[test] + fn a_state_scrolled_window_clamps_and_keeps_the_last_viewport_full() { + assert_eq!(state_scrolled_list_range(100, usize::MAX, 16, 6), 84..100); + assert_eq!(state_scrolled_list_range(5, 3, 16, 6), 0..5); + } } diff --git a/crates/nyaterm-desktop/src/features/remote/mod.rs b/crates/nyaterm-desktop/src/features/remote/mod.rs index 642bce0c..146f4f9c 100644 --- a/crates/nyaterm-desktop/src/features/remote/mod.rs +++ b/crates/nyaterm-desktop/src/features/remote/mod.rs @@ -6,7 +6,7 @@ mod remote_runtime; pub(in crate::features) use list_window::{ ACCELERATOR_PROCESS_VIEWPORT_ROWS, DOCKER_RESOURCE_VIEWPORT_ROWS, DOCKER_VIEWPORT_ROWS, - PROCESS_VIEWPORT_ROWS, max_list_offset, + PROCESS_VIEWPORT_ROWS, max_list_offset, state_scrolled_list_range, }; pub(in crate::features) use remote_runtime::remote_refresh_due; mod state;