Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion crates/ui-egui/src/chrome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,13 @@ pub fn status_bar(app: &mut RustGisApp, ui: &mut Ui) {
// Pointer coordinates.
let format = app.map.coord_format;
let earth = app.session.map().body().is_earth();
// Kept after the pointer leaves the map, so a click here still shows the position;
// dimmed then, to tell it from a live one.
let kept = app.map.kept_pointer_lonlat(app.session.map().instance_id(), app.session.opened);
let live = app.map.pointer_lonlat.is_some();
let resp = ui.add(
egui::Label::new(
egui::RichText::new(format.readout(app.map.pointer_lonlat, earth)).monospace().size(11.5).color(t.text_dim),
egui::RichText::new(format.readout(kept, earth)).monospace().size(11.5).color(if live { t.text_dim } else { t.text_disabled }),
)
.sense(Sense::click()),
);
Expand Down
19 changes: 19 additions & 0 deletions crates/ui-egui/src/mapview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ pub struct MapView {
/// A map or layout waiting for the user to confirm its deletion.
pub deleting: Option<Deletion>,
pub pointer_lonlat: Option<Coord>,
/// The last pointer position seen over a map, kept for the status bar readout after the
/// pointer leaves the map; with the map's instance and the session's `opened` it belongs to.
kept_lonlat: Option<(Coord, u64, u64)>,
/// Where the map's context menu was opened: the pointer leaves the map for the menu.
pub menu_lonlat: Option<Coord>,
/// The text graphic the context menu was opened on (layer id, row), for Edit Text.
Expand Down Expand Up @@ -403,6 +406,7 @@ impl Default for MapView {
popup_rev: 0,
deleting: None,
pointer_lonlat: None,
kept_lonlat: None,
menu_lonlat: None,
menu_text: None,
coord_format: crate::CoordReadout::Decimal,
Expand Down Expand Up @@ -437,6 +441,18 @@ impl Default for MapView {
}

impl MapView {
/// The position the status bar shows: the last one over this map, kept while the pointer is
/// elsewhere. A position from another map or project no longer applies, so it is dropped.
pub fn kept_pointer_lonlat(&mut self, map_instance: u64, opened: u64) -> Option<Coord> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness (low confidence, about 30%): the stale slot is only cleared lazily, and only when kept_pointer_lonlat runs. That call sits in chrome::status_bar, which is skipped in backstage (lib.rs ~5245). If the map changes while backstage is open, with no opened bump and no hover, then after returning the readout may show the old position on the original map. That contradicts the "switching maps puts the readout back to —" claim in the PR body and maps.md.

I did not verify that a map switch can happen while backstage is open. A cheap guard is to call kept_pointer_lonlat once per frame, before the backstage branch in lib.rs, so the slot is cleared regardless of which view is drawn. Alternatively, drop the stale entry in the map-switch path directly.

match self.kept_lonlat {
Some((c, m, o)) if m == map_instance && o == opened => Some(c),
_ => {
self.kept_lonlat = None;
None
}
}
}

/// Whether an exact frame is being composed off the UI thread, or the frame shown waits for
/// tile pictures still to be drawn.
pub fn drawing(&self) -> bool {
Expand Down Expand Up @@ -2257,6 +2273,9 @@ fn interact(app: &mut RustGisApp, ui: &mut Ui, resp: &egui::Response, rect: Rect
let time = now(ui);
let hover = resp.hover_pos();
app.map.pointer_lonlat = hover.and_then(|p| screen_to_lonlat(app, rect, p));
if let Some(c) = app.map.pointer_lonlat.filter(|c| c.is_finite()) {
app.map.kept_lonlat = Some((c, app.session.map().instance_id(), app.session.opened));
}
let tool = app.session.tool;
cancel_changed_selection(app, rect);

Expand Down
76 changes: 76 additions & 0 deletions crates/ui-egui/tests/coordinate_readout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,79 @@ fn the_status_bar_reads_the_pointer_in_the_chosen_format_and_a_click_switches()
hover_map(&mut h);
h.get_by_label(&rustgis_engine::geom::format_lonlat(h.state().map.pointer_lonlat.unwrap()));
}

/// Moves the mouse onto the status bar, off the map, for a few frames.
fn hover_status_bar(h: &mut egui_kittest::Harness<'_, RustGisApp>) {
for _ in 0..3 {
h.input_mut().events.push(egui::Event::PointerMoved(egui::pos2(800.0, 892.0)));
h.step();
}
}

fn readout_at_center(h: &mut egui_kittest::Harness<'_, RustGisApp>) -> String {
h.state_mut().run("map.goToXY", json!({"lon": 44.4, "lat": 33.3, "scale": 10000})).unwrap();
hover_map(h);
rustgis_engine::geom::format_lonlat(h.state().map.pointer_lonlat.expect("the pointer is over the map"))
}

#[test]
fn the_readout_is_a_dash_until_the_pointer_has_been_over_the_map() {
let mut h = harness(RustGisApp::with_sample());
h.step();
h.get_by_label("—");
hover_status_bar(&mut h);
h.get_by_label("—");
assert!(h.state().map.pointer_lonlat.is_none());
}

#[test]
fn the_position_stays_when_the_pointer_leaves_the_map_and_a_click_switches_format() {
let mut h = harness(RustGisApp::with_sample());
let decimal = readout_at_center(&mut h);
hover_status_bar(&mut h);
assert!(h.state().map.pointer_lonlat.is_none());
h.get_by_label(&decimal);
// The pointer in the window's corner, or gone altogether, keeps it too.
h.input_mut().events.push(egui::Event::PointerGone);
h.step();
h.get_by_label(&decimal);
// A click on the readout shows the same position in the next format.
h.get_by_label(&decimal).click();
h.step();
h.step();
assert_eq!(h.state().map.coord_format, CoordReadout::Dms);
let at = h.state().map.pointer_lonlat;
assert!(at.is_none());
h.get_by_label_contains("44°");
// The pointer back over the map follows it again.
hover_map(&mut h);
assert!(h.state().map.pointer_lonlat.is_some());
}

#[test]
fn the_position_is_dropped_on_another_map_or_project() {
let mut h = harness(RustGisApp::with_sample());
let decimal = readout_at_center(&mut h);
hover_status_bar(&mut h);
h.get_by_label(&decimal);
// Another map becoming the active one.
h.state_mut().run("map.new", json!({"name": "Second Map"})).unwrap();
h.step();
h.step();
assert!(h.query_by_label(&decimal).is_none());
h.get_by_label("—");
// Back on the first map it is not remembered from before.
h.state_mut().run("map.activate", json!({"name": "World Atlas"})).ok();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (test): this check can never fail, so it doesn't test "back on the first map".

The sample's first map is named "World" (crates/engine/src/sample.rs:72), not "World Atlas", which is the project name. So map.activate errors, and .ok() swallows the error. The active map stays "Second Map", and the — assertion on the next line passes trivially.

Use the real name and let a failure surface:

Suggested change
h.state_mut().run("map.activate", json!({"name": "World Atlas"})).ok();
h.state_mut().run("map.activate", json!({"name": "World"})).unwrap();

Confidence: high (the name mismatch is verified against sample.rs).

h.step();
h.step();
h.get_by_label("—");
// A new position, then another project replacing this one.
let decimal = readout_at_center(&mut h);
hover_status_bar(&mut h);
h.get_by_label(&decimal);
h.state_mut().run("project.new", json!({})).unwrap();
h.step();
h.step();
assert!(h.query_by_label(&decimal).is_none());
h.get_by_label("—");
}
7 changes: 5 additions & 2 deletions docs/user-guide/maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,11 @@ and **View In Google Earth** open the place in your browser at about the same zo

### Coordinate readouts

The status bar shows the pointer's coordinates. Click them to step through four formats; the
choice is remembered and also sets the right-click menu's first row. The same place in each:
The status bar shows the pointer's coordinates while it is over the map, and keeps the last
position (dimmed) when it leaves, so you can move to the status bar and click it. It reads `—`
until the pointer has been over the map, and again after you switch to another map or open another
project. Click the coordinates to step through four formats; the choice is remembered and also
sets the right-click menu's first row. The same place in each:

| Format | Example | |
| --- | --- | --- |
Expand Down
Loading