mirror of
https://github.com/nyakang/nyaterm.git
synced 2026-09-21 16:01:29 +00:00
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user