Skip to content

Commit 371d204

Browse files
alkaidmuliangcong
andauthored
fix: scroll left panel (#257)
Co-authored-by: alkaid <muliangcong@126.com>
1 parent 05c38ed commit 371d204

2 files changed

Lines changed: 110 additions & 7 deletions

File tree

app/src/ssh_manager/panel.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ use warp_core::ui::theme::color::internal_colors;
1919
use warpui::elements::{
2020
AcceptedByDropTarget, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius,
2121
CrossAxisAlignment, Dismiss, Draggable, DraggableState, DropTarget, DropTargetData, Element,
22-
Empty, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
23-
ParentAnchor, ParentElement, ParentOffsetBounds, Radius, SavePosition, Stack, Text,
22+
Empty, Fill as ElementFill, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
23+
OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, SavePosition,
24+
ScrollbarWidth, Stack, Text,
2425
};
26+
use warpui::elements::{ClippedScrollStateHandle, ClippedScrollable};
2527
use warpui::platform::Cursor;
2628
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
2729
use warpui::{
@@ -194,6 +196,8 @@ pub struct SshManagerPanel {
194196
/// 区段头的 Refresh / Toggle 按钮 hover state。
195197
candidates_refresh_btn: MouseStateHandle,
196198
candidates_toggle_btn: MouseStateHandle,
199+
200+
content_scroll_state: ClippedScrollStateHandle,
197201
}
198202

199203
impl SshManagerPanel {
@@ -220,6 +224,7 @@ impl SshManagerPanel {
220224
candidate_add_states: HashMap::new(),
221225
candidates_refresh_btn: MouseStateHandle::default(),
222226
candidates_toggle_btn: MouseStateHandle::default(),
227+
content_scroll_state: ClippedScrollStateHandle::default(),
223228
};
224229
// 面板首次打开 → 立刻读一次 ssh_config(PRODUCT.md decision A)。
225230
me.candidates.update(ctx, |vm, ctx| vm.refresh(ctx));
@@ -1827,17 +1832,30 @@ impl View for SshManagerPanel {
18271832
.with_padding_right(PANEL_HORIZONTAL_PADDING - ITEM_PADDING_HORIZONTAL)
18281833
.finish();
18291834

1830-
// 让 tree 占满剩余垂直空间 — 这样 root DropTarget 覆盖到 panel 底部,
1831-
// 用户在树最底下空白处拖也能落到 root(`SshDropData{parent_id:None}`)。
1832-
let tree_filled = warpui::elements::Shrinkable::new(1.0, tree).finish();
1835+
let root_content = Flex::column()
1836+
.with_main_axis_size(MainAxisSize::Min)
1837+
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
1838+
.with_child(candidates_section)
1839+
.with_child(tree)
1840+
.finish();
1841+
1842+
let scrollable_content = ClippedScrollable::vertical_centered(
1843+
self.content_scroll_state.clone(),
1844+
root_content,
1845+
ScrollbarWidth::Custom(4.0),
1846+
appearance.theme().nonactive_ui_detail().into(),
1847+
appearance.theme().active_ui_detail().into(),
1848+
ElementFill::None,
1849+
)
1850+
.with_overlayed_scrollbar()
1851+
.finish();
18331852

18341853
let panel_content = Container::new(
18351854
Flex::column()
18361855
.with_main_axis_size(MainAxisSize::Max)
18371856
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
18381857
.with_child(toolbar)
1839-
.with_child(candidates_section)
1840-
.with_child(tree_filled)
1858+
.with_child(warpui::elements::Shrinkable::new(1.0, scrollable_content).finish())
18411859
.finish(),
18421860
)
18431861
.finish();

app/src/ssh_manager/panel_tests.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
55
use super::*;
66
use chrono::NaiveDateTime;
7+
use pathfinder_geometry::vector::vec2f;
8+
use warp_core::ui::appearance::Appearance;
79
use warp_ssh_manager::{NodeKind, SshNode};
10+
use warpui::platform::WindowStyle;
11+
use warpui::units::IntoPixels;
12+
use warpui::{App, Presenter, WindowInvalidation};
13+
14+
use crate::test_util::settings::initialize_settings_for_tests;
815

916
// --- 测试辅助 --------------------------------------------------------------
1017

@@ -38,6 +45,57 @@ fn server(id: &str, parent_id: Option<&str>, name: &str, sort_order: i32) -> Ssh
3845
}
3946
}
4047

48+
fn panel_with_nodes(
49+
ctx: &mut ViewContext<SshManagerPanel>,
50+
nodes: Vec<SshNode>,
51+
) -> SshManagerPanel {
52+
let depths = compute_depths(&nodes);
53+
let candidates = ctx.add_model(|_| CandidatesViewModel::new());
54+
let mut row_states = HashMap::new();
55+
let mut row_drag_states = HashMap::new();
56+
for node in &nodes {
57+
row_states.insert(node.id.clone(), MouseStateHandle::default());
58+
row_drag_states.insert(node.id.clone(), DraggableState::default());
59+
}
60+
61+
SshManagerPanel {
62+
nodes,
63+
depths,
64+
selected_id: None,
65+
add_folder_btn: MouseStateHandle::default(),
66+
add_server_btn: MouseStateHandle::default(),
67+
toggle_all_btn: MouseStateHandle::default(),
68+
row_states,
69+
row_drag_states,
70+
context_menu_position: None,
71+
context_menu_target: None,
72+
context_menu_item_states: (0..MAX_CONTEXT_MENU_ITEMS)
73+
.map(|_| MouseStateHandle::default())
74+
.collect(),
75+
rename_state: None,
76+
candidates,
77+
candidate_row_states: HashMap::new(),
78+
candidate_add_states: HashMap::new(),
79+
candidates_refresh_btn: MouseStateHandle::default(),
80+
candidates_toggle_btn: MouseStateHandle::default(),
81+
content_scroll_state: ClippedScrollStateHandle::default(),
82+
}
83+
}
84+
85+
fn render_panel_scene(app: &mut App, presenter: &mut Presenter, window_id: warpui::WindowId) {
86+
let mut updated = std::collections::HashSet::new();
87+
updated.insert(app.root_view_id(window_id).unwrap());
88+
let invalidation = WindowInvalidation {
89+
updated,
90+
..Default::default()
91+
};
92+
93+
app.update(move |ctx| {
94+
presenter.invalidate(invalidation, ctx);
95+
presenter.build_scene(vec2f(240., 120.), 1., None, ctx);
96+
});
97+
}
98+
4199
// --- resolve_parent_for_new_node 测试 ---------------------------------------
42100

43101
#[test]
@@ -252,3 +310,30 @@ fn sort_keeps_orphaned_existing_nodes_visible_as_roots() {
252310
assert_eq!(depths["s1"], 0);
253311
assert_eq!(depths["f1"], 0);
254312
}
313+
314+
#[test]
315+
fn panel_content_can_scroll_when_ssh_list_is_taller_than_panel() {
316+
App::test((), |mut app| async move {
317+
initialize_settings_for_tests(&mut app);
318+
app.add_singleton_model(|_| Appearance::mock());
319+
app.add_singleton_model(|_| SshTreeChangedNotifier::new());
320+
321+
let nodes = (0..60)
322+
.map(|i| server(&format!("s{i}"), None, &format!("server-{i}"), i))
323+
.collect::<Vec<_>>();
324+
let (window_id, panel) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
325+
panel_with_nodes(ctx, nodes)
326+
});
327+
let mut presenter = Presenter::new(window_id);
328+
329+
render_panel_scene(&mut app, &mut presenter, window_id);
330+
let scroll_state = panel.read(&app, |panel, _| panel.content_scroll_state.clone());
331+
332+
scroll_state.scroll_by(10_000_f32.into_pixels());
333+
render_panel_scene(&mut app, &mut presenter, window_id);
334+
335+
let scroll_start = scroll_state.scroll_start().as_f32();
336+
assert!(scroll_start > 0.0);
337+
assert!(scroll_start < 10_000.0);
338+
});
339+
}

0 commit comments

Comments
 (0)