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
62 changes: 31 additions & 31 deletions crates/ui-egui/src/mapview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2844,40 +2844,40 @@ fn popup(app: &mut RustGisApp, ui: &mut Ui, rect: Rect) {
.corner_radius(CornerRadius { nw: 10, ne: 10, sw: 0, se: 0 })
.inner_margin(egui::Margin::symmetric(12, 8))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(egui::RichText::new(&title).font(semibold(14.0)));
ui.label(egui::RichText::new(&layer).size(11.0).color(t.text_dim));
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
if ui.add(egui::Button::new(tl!("×")).frame(false)).clicked() {
close = true;
// Lay out controls first; the title and layer share only the width left.
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {

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.

Possible visual regression: header controls are now top-aligned. Before, the controls sat in a ui.horizontal (centre cross-axis), so × / zoom / ‹ › were vertically centred against the two-line title block. Now right_to_left(Align::Min) pins them to the top. Other header rows in this crate use right_to_left(Align::Center) (e.g. panes.rs:612, chrome.rs:342), which suggests centring is the house style. Please compare the "after" screenshot against the old placement. If the controls moved, egui::Align::Center is the likely fix, though it's worth checking that egui centres controls laid out before the title block. Confidence: medium (I could not render it).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Compared the attached before/after renders: controls remain aligned with the first title line. The previous inner control layout also used Align::Min; retaining the current layout. (reviewed at 5e660e8)

if ui.add(egui::Button::new(tl!("×")).frame(false)).clicked() {
close = true;
}
// A feature (not a raster pixel): zoom to it.
if shown.is_some() {
let (r, resp) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::click());
let ink = if resp.hovered() { t.text } else { t.icon };
crate::icons::paint(ui.painter(), r, "zoom_sel", ink, t.accent);
if resp.on_hover_text(tl!("Zoom To")).clicked() {
zoom = true;
}
// A feature (not a raster pixel): zoom to it.
if shown.is_some() {
let (r, resp) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::click());
let ink = if resp.hovered() { t.text } else { t.icon };
crate::icons::paint(ui.painter(), r, "zoom_sel", ink, t.accent);
if resp.on_hover_text(tl!("Zoom To")).clicked() {
zoom = true;
}
}
// Several features identified: step through them.
if results > 1 {
if ui.add_enabled(index + 1 < results, egui::Button::new("›").frame(false)).clicked() {
step = Some(index + 1);
}
// Several features identified: step through them.
if results > 1 {
if ui.add_enabled(index + 1 < results, egui::Button::new("›").frame(false)).clicked() {
step = Some(index + 1);
}
let n = (index + 1).to_string();
let total = results.to_string();
ui.label(
egui::RichText::new(crate::i18n::fmt("{n} of {total}", &[("n", &n), ("total", &total)]))
.size(11.0)
.color(t.text_dim),
);
if ui.add_enabled(index > 0, egui::Button::new("‹").frame(false)).clicked() {
step = Some(index.saturating_sub(1));
}
let n = (index + 1).to_string();
let total = results.to_string();
ui.label(
egui::RichText::new(crate::i18n::fmt("{n} of {total}", &[("n", &n), ("total", &total)]))
.size(11.0)
.color(t.text_dim),
);
if ui.add_enabled(index > 0, egui::Button::new("‹").frame(false)).clicked() {
step = Some(index.saturating_sub(1));
}
}
ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
ui.add(egui::Label::new(egui::RichText::new(&title).font(semibold(14.0))).truncate()).on_hover_text(&title);
ui.add(egui::Label::new(egui::RichText::new(&layer).size(11.0).color(t.text_dim)).truncate())
.on_hover_text(&layer);
});
});
});
Expand Down
47 changes: 47 additions & 0 deletions crates/ui-egui/tests/smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2917,6 +2917,53 @@ fn dynamic_text_is_a_menu_of_values_that_starts_the_text() {
assert_eq!(h.state().ui.dynamic_text, None);
}

#[test]
fn popup_titles_leave_room_for_controls() {
use egui::accesskit::Role;

for title in ["Oak".to_string(), "HIDDEN INTERSECTION Tab Sign (Oversized) ".repeat(4), "長い地物名".repeat(30)] {
for count in [1, 2] {
let mut h = harness();
h.ctx.set_missing_glyph_policy(egui::MissingGlyphPolicy::Tofu);
let features: Vec<_> = (0..count)
.map(|_| json!({"type": "Feature", "properties": {"name": title}, "geometry": {"type": "Point", "coordinates": [0, 0]}}))
.collect();
h.state_mut()
.run(
"layer.addDataText",
json!({
"fileName": "popup.geojson",
"name": "A layer with a name much longer than the popup header can hold",

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.

The layer name is set to a long string here, but the test never asserts on it. Only the feature title is checked against the close, previous and next buttons. The PR description says layer names are bounded too, so this is a coverage gap. Consider also finding the layer label (h.query_all_by_label(layer), with layer bound to this string) and asserting layer_rect.max.x < close.min.x. Confidence: medium. This is a test-coverage gap, not a runtime bug.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Both labels use the same bounded column, and the rendered long-layer case was inspected. Keeping the regression focused on title overlap and control interactions. (reviewed at 5e660e8)

"text": json!({"type": "FeatureCollection", "features": features}).to_string()
}),
)
.unwrap();
h.state_mut().run("map.zoomToExtent", json!({"extent": [-5, -4, 5, 4]})).unwrap();
h.ctx.data_mut(|d| d.insert_temp(egui::Id::unique("identify_popup_limit"), egui::vec2(240.0, 360.0)));
h.state_mut().run("ui.identify", json!({"lon": 0, "lat": 0})).unwrap();
h.run_steps(6);

let title_rect = h.query_all_by_label(&title).map(|n| n.rect()).min_by(|a, b| a.min.y.total_cmp(&b.min.y)).unwrap();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let close = h.get_by_role_and_label(Role::Button, "×").rect();
assert!(close.max.x <= title_rect.min.x - 12.0 + 240.0, "close must stay inside the popup");

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.

Bound is looser than it claims. The popup's right edge is title_left - 12 + 240, but the header's right content edge sits 12px inside that (the header's inner margin). So this assertion would pass even if the close button overflowed the popup's right padding by up to 12px. The 240 also duplicates the identify_popup_limit value set a few lines up. Suggested tightening:

Suggested change
assert!(close.max.x <= title_rect.min.x - 12.0 + 240.0, "close must stay inside the popup");
assert!(close.max.x <= title_rect.min.x - 12.0 + 240.0 - 12.0, "close must stay inside the popup");

Confidence: medium.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This assertion intentionally checks outer popup containment; exact right padding is not its contract. Separate assertions check title/control separation. (reviewed at 5e660e8)

assert!(title_rect.max.x < close.min.x, "title must leave room for close");
if count > 1 {
let previous = h.get_by_role_and_label(Role::Button, "‹").rect();
assert!(title_rect.max.x < previous.min.x, "title must leave room for all navigation controls");
h.get_by_role_and_label(Role::Button, "›").click();
h.run_steps(3);
assert_eq!(h.state().map.popup.as_ref().unwrap().index, 1);
h.get_by_role_and_label(Role::Button, "‹").click();
h.run_steps(3);
assert_eq!(h.state().map.popup.as_ref().unwrap().index, 0);
}
h.get_by_role_and_label(Role::Button, "×").click();
h.run_steps(3);
assert!(h.state().map.popup.is_none(), "close must remain clickable");
}
}
}

#[test]
fn the_popup_closes_when_its_feature_is_deleted() {
let mut h = harness();
Expand Down
3 changes: 2 additions & 1 deletion docs/user-guide/editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ Pop-ups (Home › Navigate) choose which layers a click with Explore identifies:
under the click, every visible layer, only selectable layers, only the layer selected in Layers, or
nothing. The pop-up lists every feature under the click in those layers, overlapping ones included
(up to 100): step through them with ‹ ›. The feature shown is the one selected. The magnifier in
the pop-up's header zooms to it.
the pop-up's header zooms to it. Long feature titles and layer names are shortened with an
ellipsis to leave room for the header controls; hover over either name to read it in full.

The topmost and visible modes also identify the features of vector tile layers under the click:
PMTiles and MBTiles archives and vector tiles added as data (not catalog basemaps or MapLibre
Expand Down
Loading