Server list: Up and Down select, Shift moves the server

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
Federico TaglianiandClaude Sonnet 5.5 committed 2026-11-02 00:09:18 +01:00
1 parent f8bfa06b90
commit e30335f719
4 files changed
+114 -3

No files matched your search

+2 -1
View File
@@ -17566,8 +17566,9 @@ impl Game<'_> {
| KeyCode::ArrowUp
| KeyCode::ArrowDown),
) => {
let shift = self.has_shift_down();
if let Some(t) = self.title.as_mut() {
t.arrow_key(key, (w, h));
t.arrow_key(key, (w, h), shift);
}
}
PhysicalKey::Code(KeyCode::Backspace) => self.title_edit(None, true),
@@ -293,7 +293,7 @@ impl StartupMenu {
| KeyCode::ArrowUp
| KeyCode::ArrowDown),
) => {
title.arrow_key(key, viewport);
title.arrow_key(key, viewport, self.modifiers.shift_key());
}
_ => {
let cleared = !title.has_caret_field()
+103
View File
@@ -1633,6 +1633,60 @@ impl Screen {
Outcome::None
}
/// The mouse wheel over the server list (`scrollRate`: half a row).
/// Up or Down on the list (`AbstractSelectionList.nextEntry`): the next
/// server or LAN game, the first (Down) or last (Up) with none selected, and
/// none past the ends. With Shift a saved server swaps with its neighbour
/// (`OnlineServerEntry.keyPressed`) and stays selected. False when the screen
/// is not the list or has no entries.
pub fn arrow(&mut self, up: bool, shift: bool, size: (f32, f32), saves: &Path) -> bool {
let Self::List {
servers,
selected,
lan,
scroll,
..
} = self
else {
return false;
};
let total = servers.len() + lan.games().len();
if total == 0 {
return false;
}
let next = match *selected {
Some(k) if shift => {
// A LAN game has no place to move to.
let to = if up { k.checked_sub(1) } else { Some(k + 1) };
if let Some(to) = to.filter(|&to| k < servers.len() && to < servers.len()) {
servers.swap(k, to);
save_servers(&servers_file(saves), servers);
Some(to)
} else {
Some(k)
}
}
Some(k) if up => Some(k.saturating_sub(1)),
Some(k) => Some((k + 1).min(total - 1)),
None if shift => None,
None => Some(if up { total - 1 } else { 0 }),
};
*selected = next;
if let Some(k) = next {
// scrollToEntry: the row, with the LAN header between the servers
// and the games, into view.
let row = if k < servers.len() { k } else { k + 1 };
let (top, bottom, _) = list_area(size.0.ceil(), size.1.ceil());
let content = content_height(servers.len(), lan.games().len());
let (row_top, view) = (2.0 + row as f32 * ROW_HEIGHT, bottom - top);
if row_top < scroll.amount {
scroll.amount = row_top;
} else if row_top + ROW_HEIGHT > scroll.amount + view {
scroll.amount = row_top + ROW_HEIGHT - view;
}
scroll.clamp(top, bottom, content);
}
true
}
pub fn wheel(&mut self, delta: f32, cursor: (f32, f32), size: (f32, f32)) {
if let Self::List {
servers,
@@ -2225,6 +2279,55 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn arrow_keys_select_servers_and_shift_moves_them() {
let dir = std::env::temp_dir().join(format!("horizon-mp-keys-{}", std::process::id()));
let saves = dir.join("saves");
std::fs::create_dir_all(&saves).unwrap();
let file = servers_file(&saves);
let list: Vec<ServerEntry> = ["a", "b", "c"]
.iter()
.map(|n| ServerEntry::new(n, "127.0.0.1:2"))
.collect();
servers::save(&file, &list).unwrap();
let mut screen = Screen::list(&saves);
let size = (427.0, 240.0);
let state = |screen: &Screen| match screen {
Screen::List {
servers, selected, ..
} => (
servers.iter().map(|s| s.name.clone()).collect::<Vec<_>>(),
*selected,
),
_ => unreachable!(),
};
let abc = vec!["a".to_owned(), "b".into(), "c".into()];
// None selected: Down takes the first, as vanilla's nextEntry does.
assert!(screen.arrow(false, false, size, &saves));
assert_eq!(state(&screen), (abc.clone(), Some(0)));
// Up at the top stays; Down walks; the last stays.
screen.arrow(true, false, size, &saves);
assert_eq!(state(&screen).1, Some(0));
screen.arrow(false, false, size, &saves);
screen.arrow(false, false, size, &saves);
screen.arrow(false, false, size, &saves);
assert_eq!(state(&screen).1, Some(2));
// Shift+Up moves "c" above "b", keeps it selected and saves the order.
screen.arrow(true, true, size, &saves);
assert_eq!(
state(&screen),
(vec!["a".to_owned(), "c".into(), "b".into()], Some(1))
);
let saved: Vec<String> = servers::load(&file).into_iter().map(|s| s.name).collect();
assert_eq!(saved, ["a", "c", "b"]);
// Nothing moves past the first server.
screen.arrow(true, true, size, &saves);
screen.arrow(true, true, size, &saves);
assert_eq!(state(&screen).1, Some(0));
assert_eq!(state(&screen).0[0], "c");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn icon_arrows_join_and_move_servers() {
let dir = std::env::temp_dir().join(format!("horizon-mp-arrows-{}", std::process::id()));
+8 -1
View File
@@ -3890,7 +3890,7 @@ Would you like to enable the Narrator or visit the Accessibility Settings?",
}
}
/// 2D spatial arrow key navigation across enabled buttons.
pub fn arrow_key(&mut self, key: winit::keyboard::KeyCode, size: (f32, f32)) {
pub fn arrow_key(&mut self, key: winit::keyboard::KeyCode, size: (f32, f32), shift: bool) {
use winit::keyboard::KeyCode;
if self.rules.get_mut().is_some() {
return;
@@ -3898,6 +3898,13 @@ Would you like to enable the Narrator or visit the Accessibility Settings?",
if self.has_caret_field() {
return;
}
if self.focused_button.is_none()
&& let Mode::Multiplayer(screen) = &mut self.mode
&& matches!(key, KeyCode::ArrowUp | KeyCode::ArrowDown)
&& screen.arrow(key == KeyCode::ArrowUp, shift, size, &self.saves)
{
return;
}
if self.list_focused()
&& self.focused_button.is_none()
&& let Mode::Select { worlds, selected } = &mut self.mode