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:
1 parent
931e0ed6f5
commit
5f1c8ff46a
7 files changed
+338
-145
No files matched your search
@@ -196,7 +196,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- gpui orders Tab stops by `tab_index` before paint order, and registers none for an `invisible()` element. A stop that must come first where it sits, yet draw over what paints after it, stays in flow at zero size and raises what it shows (`i18n::SkipLink`): a lower `tab_index` would sort it before the root scope's own handle, so Tab from the root would pass it.
|
||||
- Color-blind safe charts swap Ely's chart hues for a set fitted to stay apart under Machado's protanopia, deuteranopia and tritanopia; a test holds every pair 0.10 apart in Oklab and each hue 3:1 on its page (`Theme::set_color_blind_safe`, in Ely's own palettes alone).
|
||||
- gpui 0.2.2 packs pictures in its atlas without padding and samples them with a linear filter, so a scaled picture's edge takes color from its atlas neighbor, and pictures laid edge to edge show seams. A map tile carries a one-pixel rim copied from its own edge and draws one of its pixels larger each side, clipped to its box (`maps::tile`).
|
||||
- A map keeps places in `f64`: at zoom 19 the world is 134 million pixels across. Its wheel pans and Command or Control with the wheel zooms about the pointer, as the canvas's does.
|
||||
- A map keeps places in `f64`: at zoom 19 the world is 134 million pixels across. Its wheel pans and Command or Control with the wheel zooms about the pointer, as the canvas's does. Its tiles stay in gpui's asset cache, as every `img()` picture does. Its popup is an overlay that is not modal: it takes focus when it shows, and the frame it stops showing, by any way out, hands focus back.
|
||||
|
||||
## Decisions
|
||||
|
||||
|
||||
+59
-37
@@ -1,8 +1,8 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, InteractiveElement, IntoElement, ParentElement, Pixels,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
AnyElement, App, ElementId, FocusHandle, InteractiveElement, IntoElement, ParentElement,
|
||||
Pixels, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::LatLon;
|
||||
@@ -111,7 +111,7 @@ impl MapMarker {
|
||||
}
|
||||
}
|
||||
|
||||
/// A card that points at a place, over the map; its close button hands the owner `on_close`.
|
||||
/// A card that points at a place, over the map. It takes focus when it shows and hands it back when it goes; its close button and Escape hand the owner `on_close`.
|
||||
pub struct MapPopup {
|
||||
pub(crate) at: LatLon,
|
||||
title: SharedString,
|
||||
@@ -143,12 +143,13 @@ impl MapPopup {
|
||||
self.on_close.clone()
|
||||
}
|
||||
|
||||
/// The card at its point in a view of `size`; none when the point lies outside.
|
||||
/// The card at its point in a view of `size`, holding `focus`, its body scrolling past the room on its side; none when the point lies outside.
|
||||
pub(crate) fn place(
|
||||
self,
|
||||
owner: &ElementId,
|
||||
point: (f32, f32),
|
||||
size: (f32, f32),
|
||||
focus: &FocusHandle,
|
||||
window: &Window,
|
||||
cx: &App,
|
||||
) -> Option<AnyElement> {
|
||||
@@ -165,44 +166,53 @@ impl MapPopup {
|
||||
margin,
|
||||
lift,
|
||||
)?;
|
||||
let card = div()
|
||||
.id((owner.clone(), "popup"))
|
||||
.absolute()
|
||||
.left(Pixels::from(spot.left))
|
||||
.w(Pixels::from(spot.width))
|
||||
.occlude()
|
||||
let head = div()
|
||||
.flex()
|
||||
.items_start()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.p_3()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.bg(colors.overlay)
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.shadow(theme.elevation(Elevation::Floating))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(colors.fg)
|
||||
.child(Ellipsis::new(self.title)),
|
||||
)
|
||||
.children(self.content),
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(colors.fg)
|
||||
.child(Ellipsis::new(self.title)),
|
||||
)
|
||||
.when_some(self.on_close, |card, on_close| {
|
||||
card.child(
|
||||
.when_some(self.on_close, |head, on_close| {
|
||||
head.child(
|
||||
IconButton::new((owner.clone(), "popup-close"), IconName::X)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.tooltip("Close")
|
||||
.on_click(move |_, window, cx| on_close(window, cx)),
|
||||
)
|
||||
});
|
||||
let card = div()
|
||||
.id((owner.clone(), "popup"))
|
||||
.track_focus(focus)
|
||||
.absolute()
|
||||
.left(Pixels::from(spot.left))
|
||||
.w(Pixels::from(spot.width))
|
||||
.max_h(Pixels::from(spot.height))
|
||||
.occlude()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.p_3()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.bg(colors.overlay)
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.focus_ring(cx)
|
||||
.shadow(theme.elevation(Elevation::Floating))
|
||||
.child(head)
|
||||
.children(self.content.map(|content| {
|
||||
div()
|
||||
.id((owner.clone(), "popup-body"))
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.overflow_y_scroll()
|
||||
.child(content)
|
||||
}));
|
||||
Some(
|
||||
match spot.edge {
|
||||
Edge::Bottom(bottom) => card.bottom(Pixels::from(bottom)),
|
||||
@@ -220,11 +230,12 @@ pub(crate) enum Edge {
|
||||
Top(f32),
|
||||
}
|
||||
|
||||
/// Where a popup lies in a view.
|
||||
/// Where a popup lies in a view, and the most height it may take there.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct Spot {
|
||||
pub left: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub edge: Edge,
|
||||
}
|
||||
|
||||
@@ -241,12 +252,17 @@ pub(crate) fn spot(
|
||||
}
|
||||
let width = width.min(w - margin * 2.0).max(0.0);
|
||||
let left = (x - width / 2.0).clamp(margin, (w - margin - width).max(margin));
|
||||
let edge = if y > h / 2.0 {
|
||||
Edge::Bottom(h - y + lift)
|
||||
let (edge, height) = if y > h / 2.0 {
|
||||
(Edge::Bottom(h - y + lift), y - lift - margin)
|
||||
} else {
|
||||
Edge::Top(y + margin)
|
||||
(Edge::Top(y + margin), h - y - margin * 2.0)
|
||||
};
|
||||
Some(Spot { left, width, edge })
|
||||
Some(Spot {
|
||||
left,
|
||||
width,
|
||||
height: height.max(0.0),
|
||||
edge,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -263,6 +279,7 @@ mod tests {
|
||||
Some(Spot {
|
||||
left: 80.0,
|
||||
width: 240.0,
|
||||
height: 210.0,
|
||||
edge: Edge::Bottom(82.0)
|
||||
})
|
||||
);
|
||||
@@ -277,9 +294,14 @@ mod tests {
|
||||
"held off the right edge"
|
||||
);
|
||||
assert_eq!(
|
||||
at(200.0, 40.0).map(|spot| spot.edge),
|
||||
Some(Edge::Top(48.0)),
|
||||
"a point up top opens below"
|
||||
at(200.0, 40.0).map(|spot| (spot.edge, spot.height)),
|
||||
Some((Edge::Top(48.0), 244.0)),
|
||||
"a point up top opens below, as tall as the room under it"
|
||||
);
|
||||
assert_eq!(
|
||||
at(200.0, 160.0).map(|spot| spot.height),
|
||||
Some(120.0),
|
||||
"above, the room over the pin"
|
||||
);
|
||||
assert_eq!(at(-1.0, 100.0), None, "a point outside draws none");
|
||||
assert_eq!(at(200.0, 301.0), None);
|
||||
|
||||
+84
-91
@@ -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");
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -4,7 +4,7 @@ use std::{
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, Asset, ImageAssetLoader, ImageSource, IntoElement, ObjectFit, ParentElement,
|
||||
AnyElement, App, Asset, ImageSource, ImgResourceLoader, IntoElement, ObjectFit, ParentElement,
|
||||
Pixels, RenderImage, Styled, StyledImage, Window, div, img,
|
||||
};
|
||||
use image::{Frame, RgbaImage};
|
||||
@@ -61,7 +61,7 @@ pub(crate) fn rimmed(image: &RenderImage) -> RenderImage {
|
||||
RenderImage::new(frames)
|
||||
}
|
||||
|
||||
/// A tile's picture in its box: its rim lies one pixel past each side, clipped, so the box shows its own pixels alone. Nothing while it loads; a broken-picture mark when it cannot.
|
||||
/// A tile's picture in its box: its rim lies one pixel past each side, clipped, so the box shows its own pixels alone. Nothing while it loads; a broken-picture mark, and gpui's log line, when it cannot.
|
||||
pub(crate) fn laid_tile(
|
||||
laid: &Laid,
|
||||
source: ImageSource,
|
||||
@@ -77,7 +77,7 @@ pub(crate) fn laid_tile(
|
||||
.overflow_hidden();
|
||||
let decoded = match source {
|
||||
ImageSource::Render(image) => Some(Ok(image)),
|
||||
ImageSource::Resource(resource) => window.use_asset::<ImageAssetLoader>(&resource, cx),
|
||||
ImageSource::Resource(resource) => window.use_asset::<ImgResourceLoader>(&resource, cx),
|
||||
_ => panic!(
|
||||
"map tile {:?}: a web address, a file or a picture drawn in code",
|
||||
laid.tile
|
||||
|
||||
+27
-11
@@ -14,7 +14,7 @@ use super::{
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
layout::seeded::use_seeded,
|
||||
primitives::{FocusRing, IconName, tab_stop},
|
||||
primitives::{FocusRing, IconName, Takeover, give_back, tab_stop, take_focus},
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
};
|
||||
|
||||
@@ -28,14 +28,15 @@ struct Pan {
|
||||
owner: EntityId,
|
||||
}
|
||||
|
||||
/// The map's box, and the pointer and viewport at the press of a pan under way.
|
||||
/// The map's box, the pointer and viewport at the press of a pan under way, and the focus a shown popup took.
|
||||
#[derive(Default)]
|
||||
struct Stage {
|
||||
bounds: Bounds<Pixels>,
|
||||
grip: Option<(Point<Pixels>, MapViewport)>,
|
||||
takeover: Option<Entity<Takeover>>,
|
||||
}
|
||||
|
||||
/// A map of tiles the host supplies by zoom, column and row, with pins and a popup over them. A drag or the wheel pans it; Command or Control with the wheel zooms about the pointer; focused, the arrows pan and + and - zoom, as its buttons do. The owner keeps the viewport; a new one from the owner shows at once. It fills its box; the host gives it a height.
|
||||
/// A map of tiles the host supplies by zoom, column and row, with pins and a popup over them; a pin takes a press only while its place is in view. A drag or the wheel pans it; Command or Control with the wheel zooms about the pointer; focused, the arrows pan and + and - zoom, as its buttons do. The owner keeps the viewport; a new one from the owner shows at once. It fills its box; the host gives it a height.
|
||||
#[derive(IntoElement)]
|
||||
pub struct MapView {
|
||||
id: ElementId,
|
||||
@@ -138,12 +139,31 @@ impl RenderOnce for MapView {
|
||||
.collect(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
let shown = self
|
||||
.popup
|
||||
.map(|popup| (view.to_view(popup.at, size, tile), popup))
|
||||
.filter(|((x, y), _)| (0.0..=size.0).contains(x) && (0.0..=size.1).contains(y));
|
||||
let closer = shown.as_ref().and_then(|(_, popup)| popup.closer());
|
||||
let popup = match shown {
|
||||
Some((at, popup)) => {
|
||||
let takeover = take_focus((id.clone(), "popup"), window, cx);
|
||||
stage.update(cx, |stage, _| stage.takeover = Some(takeover.clone()));
|
||||
let held = takeover.read(cx).focus.clone();
|
||||
popup.place(&id, at, size, &held, window, cx)
|
||||
}
|
||||
None => {
|
||||
if let Some(taken) = stage.update(cx, |stage, _| stage.takeover.take()) {
|
||||
give_back(&taken, window, cx);
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let set: OnMapViewport = {
|
||||
let (local, on_viewport) = (local.clone(), self.on_viewport.clone());
|
||||
Rc::new(move |next, window, cx| {
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"map: {:.4}, {:.4} at zoom {:.2}",
|
||||
next.center.lat,
|
||||
next.center.lon,
|
||||
@@ -167,14 +187,10 @@ impl RenderOnce for MapView {
|
||||
.into_iter()
|
||||
.map(|marker| {
|
||||
let at = view.to_view(marker.at, size, tile);
|
||||
marker.place(&id, at, self.on_marker.clone(), cx)
|
||||
let seen = (0.0..=size.0).contains(&at.0) && (0.0..=size.1).contains(&at.1);
|
||||
marker.place(&id, at, self.on_marker.clone().filter(|_| seen), cx)
|
||||
})
|
||||
.collect();
|
||||
let closer = self.popup.as_ref().and_then(MapPopup::closer);
|
||||
let popup = self.popup.and_then(|popup| {
|
||||
let at = view.to_view(popup.at, size, tile);
|
||||
popup.place(&id, at, size, window, cx)
|
||||
});
|
||||
let zoom = |levels: f64, key: &'static str, icon: IconName, words: &'static str| {
|
||||
let (now, set) = (now.clone(), set.clone());
|
||||
let (least, most) = MapViewport::ZOOMS;
|
||||
@@ -322,9 +338,9 @@ impl RenderOnce for MapView {
|
||||
.on_drop(move |_: &Pan, _, cx| dropped.update(cx, |stage, _| stage.grip = None))
|
||||
.children(tiles)
|
||||
.children(markers)
|
||||
.children(popup)
|
||||
.child(controls)
|
||||
.children(attribution)
|
||||
.children(popup)
|
||||
.child(measured)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -291,8 +291,8 @@
|
||||
## T41a Maps · View — `src/maps`
|
||||
|
||||
- [x] MapView — host tiles → `maps::MapView`: the host's tile for each zoom, column and row, the nearest cut scaled past its zooms; a drag, the wheel or the arrows pan, Command-wheel, + and - or its buttons zoom
|
||||
- [x] MapMarker → `maps::MapMarker`, a pin keyed by its place's name, pressable when the map hears it
|
||||
- [x] MapPopup → `maps::MapPopup`, over or under its point toward the larger half, held inside the map
|
||||
- [x] MapMarker → `maps::MapMarker`, a pin keyed by its place's name, pressable while in view when the map hears it
|
||||
- [x] MapPopup → `maps::MapPopup`, over or under its point toward the larger half, held inside the map, its body scrolling past the room; it takes focus when it shows and hands it back when it goes
|
||||
- [x] LocationPicker → `maps::LocationPicker`, the place under a center crosshair
|
||||
- [x] CoordinateDisplay → `maps::CoordinateDisplay`, degrees or degrees, minutes and seconds, copyable
|
||||
|
||||
|
||||
Reference in new issue
Block a user