refactor(remote): streamline scrolling logic for process and Docker panels (#732)

- Replaced manual calculation of visible rows with a new `state_scrolled_list_range` function for better management of scroll offsets in the processes and Docker containers panels.
- Removed unnecessary padding calculations, simplifying the rendering logic for visible items.
- Updated related functions to utilize the new scrolling logic, enhancing performance and maintainability across the remote feature set.
This commit is contained in:
Kang
2026-09-21 12:08:31 +08:00
parent 5bff7c3165
commit 39b53595be
6 changed files with 71 additions and 107 deletions
@@ -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()
@@ -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<usize> {
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<RemoteMonitorPanel>,
) -> gpui::AnyElement {
// Tauri resource tabs: full-height virtual list + wheel offset.
@@ -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::{
@@ -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::<f32>();
let pad_bottom = filtered_processes
.iter()
.skip(window_end)
.map(row_height)
.sum::<f32>();
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.
@@ -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<usize> {
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);
}
}
@@ -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;