Maps · View: pins in view take presses, the popup hands focus back

A pin whose place lies outside the view takes no press or Tab, as its
popup would not show. The popup takes focus when it shows and, the
frame it stops showing by its button, Escape, the owner or a pan, hands
focus back to what held it, so focus stays in the map. Its body scrolls
past the room on its side and it paints over the zoom buttons. Tiles
load through gpui's logging loader, so a tile that fails says so. A
drag's owner check has a test, and the per-move viewport logs at debug.
The maps tests split into the map's and the popup's.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-27 21:57:27 -04:00
1 parent 931e0ed6f5
commit 5f1c8ff46a
7 files changed
+338 -145

No files matched your search

+84 -91
View File
@@ -6,13 +6,15 @@ use gpui::{
SharedString, Styled, TestAppContext, TouchPhase, VisualTestContext, Window, div, point, px,
};
use super::{LatLon, LocationPicker, MapMarker, MapPopup, MapView, MapViewport, Tile};
use super::{LatLon, MapMarker, MapView, MapViewport, Tile};
use crate::{
primitives::{FocusNext, FocusScope},
theme::Theme,
};
/// The view inside the map's 1px border, in pixels.
mod popup;
const SIZE: (f32, f32) = (398.0, 298.0);
const TILE: f32 = 256.0;
@@ -25,8 +27,10 @@ struct Stage {
root: FocusHandle,
part: fn(&Stage, Entity<Stage>) -> AnyElement,
views: Vec<MapViewport>,
others: Vec<MapViewport>,
said: Vec<SharedString>,
tiles: Rc<RefCell<HashSet<Tile>>>,
open: bool,
}
impl Render for Stage {
@@ -61,8 +65,10 @@ fn stage(
root: cx.focus_handle(),
part,
views: Vec::new(),
others: Vec::new(),
said: Vec::new(),
tiles: Rc::default(),
open: false,
});
settle(cx);
(host, cx)
@@ -140,6 +146,10 @@ fn focus_map(cx: &mut VisualTestContext) {
settle(cx);
}
fn moves(host: &Entity<Stage>, cx: &mut VisualTestContext) -> usize {
host.read_with(cx, |stage, _| stage.views.len())
}
#[gpui::test]
fn a_drag_pans_the_map_with_the_pointer(cx: &mut TestAppContext) {
let (host, cx) = stage(map, cx);
@@ -298,96 +308,6 @@ fn past_the_hosts_zooms_one_tile_scales(cx: &mut TestAppContext) {
);
}
fn with_popup(_: &Stage, owner: Entity<Stage>) -> AnyElement {
let closed = owner.clone();
MapView::new("map", start())
.popup(
MapPopup::new(LatLon::new(0.0, 0.0), "Null Island")
.child("Where the equator meets the prime meridian.")
.on_close(move |_, cx| say(&closed, "closed", cx)),
)
.on_viewport(move |view, _, cx| hear(&owner, view, cx))
.into_any_element()
}
#[gpui::test]
fn the_popup_closes_by_its_button_and_by_escape(cx: &mut TestAppContext) {
let (host, cx) = stage(with_popup, cx);
focus_map(cx);
press("escape", cx);
assert_eq!(said(&host, cx), ["closed"], "Escape on the map");
focus_map(cx);
press("tab", cx);
press("enter", cx);
assert_eq!(
said(&host, cx),
["closed", "closed"],
"Tab reaches the close button first"
);
}
fn far_popup(_: &Stage, owner: Entity<Stage>) -> AnyElement {
MapView::new("map", start())
.popup(MapPopup::new(LatLon::new(-60.0, 120.0), "Far").on_close(|_, _| {}))
.on_viewport(move |view, _, cx| hear(&owner, view, cx))
.into_any_element()
}
/// A popup whose place lies outside the view is not drawn, so Tab passes to the zoom button.
#[gpui::test]
fn a_popup_away_from_the_view_hides(cx: &mut TestAppContext) {
let (host, cx) = stage(far_popup, cx);
focus_map(cx);
press("tab", cx);
press("enter", cx);
assert_eq!(last(&host, cx).zoom, 3.0);
}
fn picker(_: &Stage, owner: Entity<Stage>) -> AnyElement {
LocationPicker::new("picker", MapView::new("pick-map", start()))
.on_pick(move |at, _, cx| say(&owner, &format!("{:.4},{:.4}", at.lat, at.lon), cx))
.into_any_element()
}
#[gpui::test]
fn the_picker_picks_the_center_as_the_map_moves(cx: &mut TestAppContext) {
let (host, cx) = stage(picker, cx);
cx.simulate_click(at(40.0, 40.0), Modifiers::none());
settle(cx);
press("right", cx);
press("down", cx);
let heard = said(&host, cx);
assert_eq!(heard.len(), 2, "one pick per move: {heard:?}");
let first: Vec<f64> = heard[0]
.split(',')
.map(|part| part.parse().expect("a number"))
.collect();
assert_eq!(first[0], 0.0, "right moves east along the equator");
assert!(first[1] > 0.0);
let second: Vec<f64> = heard[1]
.split(',')
.map(|part| part.parse().expect("a number"))
.collect();
assert!(
second[0] < 0.0 && second[1] == first[1],
"down moves south: {heard:?}"
);
}
fn loud(_: &Stage, owner: Entity<Stage>) -> AnyElement {
LocationPicker::new(
"picker",
MapView::new("pick-map", start()).on_viewport(move |view, _, cx| hear(&owner, view, cx)),
)
.into_any_element()
}
#[gpui::test]
#[should_panic(expected = "it hears the map's moves")]
fn a_picker_whose_map_hears_its_own_moves_fails(cx: &mut TestAppContext) {
stage(loud, cx);
}
fn nearest(_: &Stage, owner: Entity<Stage>) -> AnyElement {
MapView::new(
"map",
@@ -412,3 +332,76 @@ fn the_zoom_buttons_rest_at_the_limits(cx: &mut TestAppContext) {
fn tile_zooms_out_of_order_fail() {
let _ = MapView::new("map", start()).tile_zooms(4, 2);
}
fn far_pin(_: &Stage, owner: Entity<Stage>) -> AnyElement {
let heard = owner.clone();
MapView::new("map", start())
.markers([MapMarker::new("far", LatLon::new(-60.0, 120.0))])
.on_marker(move |key, _, cx| say(&heard, key, cx))
.on_viewport(move |view, _, cx| hear(&owner, view, cx))
.into_any_element()
}
/// A pin whose place lies outside the view takes no Tab or press, as its popup would not show.
#[gpui::test]
fn a_pin_away_from_the_view_takes_no_tab(cx: &mut TestAppContext) {
let (host, cx) = stage(far_pin, cx);
focus_map(cx);
press("tab", cx);
press("enter", cx);
assert_eq!(said(&host, cx), Vec::<SharedString>::new());
assert_eq!(last(&host, cx).zoom, 3.0, "Tab passed to the zoom button");
}
fn twins(_: &Stage, owner: Entity<Stage>) -> AnyElement {
let other = owner.clone();
div()
.flex()
.child(div().w(px(200.0)).h(px(150.0)).child(
MapView::new("west", start()).on_viewport(move |view, _, cx| hear(&owner, view, cx)),
))
.child(
div()
.w(px(200.0))
.h(px(150.0))
.child(
MapView::new("east", start()).on_viewport(move |view, _, cx| {
other.update(cx, |stage, cx| {
stage.others.push(view);
cx.notify();
})
}),
),
)
.into_any_element()
}
fn drag_line(from: (f32, f32), to: (f32, f32), cx: &mut VisualTestContext) {
let none = Modifiers::none();
cx.simulate_mouse_move(at(from.0, from.1), None, none);
cx.simulate_mouse_down(at(from.0, from.1), MouseButton::Left, none);
for step in 1..=4 {
let share = step as f32 / 4.0;
let x = from.0 + (to.0 - from.0) * share;
let y = from.1 + (to.1 - from.1) * share;
cx.simulate_mouse_move(at(x, y), Some(MouseButton::Left), none);
settle(cx);
}
cx.simulate_mouse_up(at(to.0, to.1), MouseButton::Left, none);
settle(cx);
}
/// A drag let go outside the west map leaves its grip; a drag on the east map moves the east map alone.
#[gpui::test]
fn a_drag_moves_its_own_map_alone(cx: &mut TestAppContext) {
let (host, cx) = stage(twins, cx);
drag_line((100.0, 75.0), (100.0, 280.0), cx);
let west = moves(&host, cx);
assert!(west > 0, "the west map followed its drag");
drag_line((300.0, 75.0), (330.0, 100.0), cx);
assert!(
host.read_with(cx, |stage, _| !stage.others.is_empty()),
"the east map moved"
);
assert_eq!(moves(&host, cx), west, "the west map stayed");
}
+162
View File
@@ -0,0 +1,162 @@
use gpui::{AnyElement, Entity, IntoElement, Modifiers, TestAppContext, VisualTestContext};
use super::{Stage, at, focus_map, hear, last, moves, press, said, say, settle, stage, start};
use crate::maps::{LatLon, LocationPicker, MapMarker, MapPopup, MapView};
fn with_popup(_: &Stage, owner: Entity<Stage>) -> AnyElement {
let closed = owner.clone();
MapView::new("map", start())
.popup(
MapPopup::new(LatLon::new(0.0, 0.0), "Null Island")
.child("Where the equator meets the prime meridian.")
.on_close(move |_, cx| say(&closed, "closed", cx)),
)
.on_viewport(move |view, _, cx| hear(&owner, view, cx))
.into_any_element()
}
/// A shown popup holds focus: Escape closes it, and Tab reaches its close button.
#[gpui::test]
fn the_popup_closes_by_escape_and_by_its_button(cx: &mut TestAppContext) {
let (host, cx) = stage(with_popup, cx);
press("escape", cx);
assert_eq!(said(&host, cx), ["closed"], "Escape in the card it focused");
press("tab", cx);
press("enter", cx);
assert_eq!(
said(&host, cx),
["closed", "closed"],
"Tab goes to the close button"
);
}
fn far_popup(_: &Stage, owner: Entity<Stage>) -> AnyElement {
MapView::new("map", start())
.popup(MapPopup::new(LatLon::new(-60.0, 120.0), "Far").on_close(|_, _| {}))
.on_viewport(move |view, _, cx| hear(&owner, view, cx))
.into_any_element()
}
/// A popup whose place lies outside the view is not drawn, so Tab passes to the zoom button.
#[gpui::test]
fn a_popup_away_from_the_view_hides(cx: &mut TestAppContext) {
let (host, cx) = stage(far_popup, cx);
focus_map(cx);
press("tab", cx);
press("enter", cx);
assert_eq!(last(&host, cx).zoom, 3.0);
}
fn picker(_: &Stage, owner: Entity<Stage>) -> AnyElement {
LocationPicker::new("picker", MapView::new("pick-map", start()))
.on_pick(move |at, _, cx| say(&owner, &format!("{:.4},{:.4}", at.lat, at.lon), cx))
.into_any_element()
}
#[gpui::test]
fn the_picker_picks_the_center_as_the_map_moves(cx: &mut TestAppContext) {
let (host, cx) = stage(picker, cx);
cx.simulate_click(at(40.0, 40.0), Modifiers::none());
settle(cx);
press("right", cx);
press("down", cx);
let heard = said(&host, cx);
assert_eq!(heard.len(), 2, "one pick per move: {heard:?}");
let first: Vec<f64> = heard[0]
.split(',')
.map(|part| part.parse().expect("a number"))
.collect();
assert_eq!(first[0], 0.0, "right moves east along the equator");
assert!(first[1] > 0.0);
let second: Vec<f64> = heard[1]
.split(',')
.map(|part| part.parse().expect("a number"))
.collect();
assert!(
second[0] < 0.0 && second[1] == first[1],
"down moves south: {heard:?}"
);
}
fn loud(_: &Stage, owner: Entity<Stage>) -> AnyElement {
LocationPicker::new(
"picker",
MapView::new("pick-map", start()).on_viewport(move |view, _, cx| hear(&owner, view, cx)),
)
.into_any_element()
}
#[gpui::test]
#[should_panic(expected = "it hears the map's moves")]
fn a_picker_whose_map_hears_its_own_moves_fails(cx: &mut TestAppContext) {
stage(loud, cx);
}
fn opening(stage: &Stage, owner: Entity<Stage>) -> AnyElement {
let (opened, closed) = (owner.clone(), owner.clone());
let map = MapView::new("map", start())
.markers([MapMarker::new("middle", LatLon::new(0.0, 0.0))])
.on_marker(move |_, _, cx| {
opened.update(cx, |stage, cx| {
stage.open = true;
stage.said.push("opened".into());
cx.notify();
})
})
.on_viewport(move |view, _, cx| hear(&owner, view, cx));
match stage.open {
true => map
.popup(
MapPopup::new(LatLon::new(0.0, 0.0), "Null Island").on_close(move |_, cx| {
closed.update(cx, |stage, cx| {
stage.open = false;
stage.said.push("closed".into());
cx.notify();
})
}),
)
.into_any_element(),
false => map.into_any_element(),
}
}
/// Opens the popup from its pin by keys.
fn open_by_pin(cx: &mut VisualTestContext) {
focus_map(cx);
press("tab", cx);
press("enter", cx);
}
/// The close button takes the card away and focus returns to the pin that opened it: Enter opens it again, and the arrows still pan.
#[gpui::test]
fn closing_the_popup_by_its_button_hands_focus_back(cx: &mut TestAppContext) {
let (host, cx) = stage(opening, cx);
open_by_pin(cx);
press("tab", cx);
press("enter", cx);
assert_eq!(said(&host, cx), ["opened", "closed"]);
press("enter", cx);
assert_eq!(
said(&host, cx),
["opened", "closed", "opened"],
"the pin holds focus"
);
let before = moves(&host, cx);
press("left", cx);
assert_eq!(moves(&host, cx), before + 1);
}
/// Escape takes the card away and focus returns to the pin.
#[gpui::test]
fn escape_from_the_popup_hands_focus_back(cx: &mut TestAppContext) {
let (host, cx) = stage(opening, cx);
open_by_pin(cx);
press("escape", cx);
assert_eq!(said(&host, cx), ["opened", "closed"]);
press("enter", cx);
assert_eq!(
said(&host, cx),
["opened", "closed", "opened"],
"the pin holds focus"
);
}