Parameters: show one rename tooltip per name (#10309)

Keep full names in the tooltip only when the name cell clips them, and keep
the double-click rename hint without stacking a second popup. Cover short
and long names, favorites, and double-click input with a regression test.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
MarkusRe
2026-10-31 22:48:33 +00:00
committed by GitHub
parent e0c1b8adbd
commit 4c3d1484c5
4 changed files with 75 additions and 3 deletions
+50 -2
View File
@@ -527,8 +527,8 @@ fn table(app: &mut SolveApp, ui: &mut egui::Ui, d: &mut ParamsDialog, rows: &[&R
}
_ => {
let rename = crate::i18n::tr_label("Double-click to rename");
// Always tip the full name: the Name cell clips long identifiers (#8533).
let response = ui_kit::text(ui, &r.name, ui_kit::Text::NameCell(false)).on_hover_text(format!("{}\n{rename}", r.name));
// Keep long identifiers readable (#8533) without stacking the cell's own tooltip.
let response = ui_kit::name_cell_with_hint(ui, &r.name, false, rename);
crate::scenario::publish_handle(&format!("param:name:{}", r.name), response.rect.center());
if response.double_clicked() {
d.renaming = Some((r.name.clone(), r.name.clone()));
@@ -953,6 +953,54 @@ pub fn complete(ui: &mut egui::Ui, resp: &egui::Response, text: &mut String) ->
mod tests {
use super::*;
#[test]
fn parameter_names_show_one_tooltip_and_only_repeat_clipped_names() {
fn text_in(shape: &egui::Shape, within: egui::Rect, texts: &mut Vec<String>) {
match shape {
egui::Shape::Text(text) if within.contains(text.pos) => texts.push(text.galley.text().to_owned()),
egui::Shape::Vec(shapes) => {
for shape in shapes {
text_in(shape, within, texts);
}
}
_ => {}
}
}
let long = "Driver_MountHole_pitch_diameter_".repeat(2);
for name in ["rip_length", long.as_str()] {
for favorite in [false, true] {
let mut h = crate::scenario::Harness::new();
h.app.home.open = false;
h.app.run("parameters.add", json!({"name":name,"expression":"140 mm"})).unwrap();
h.app.run("parameters.favorite", json!({"name":name,"favorite":favorite})).unwrap();
h.step(&json!({"start":"parameters.change"})).unwrap();
h.frames(6);
let at = crate::scenario::handle_at(&format!("param:name:{name}")).unwrap();
h.step(&json!({"move":[at.x,at.y]})).unwrap();
h.frames(90);
let tooltips: Vec<_> = h.ctx.memory(|m| {
m.areas()
.visible_layer_ids()
.into_iter()
.filter(|layer| layer.order == egui::Order::Tooltip)
.filter_map(|layer| m.area_rect(layer.id))
.collect()
});
assert_eq!(tooltips.len(), 1, "one name must not stack tooltips: {name}, favorite={favorite}");
let mut texts = Vec::new();
for shape in &h.shapes {
text_in(&shape.shape, tooltips[0], &mut texts);
}
let hint = crate::i18n::tr_label("Double-click to rename");
let expected = if name == "rip_length" { hint.to_owned() } else { format!("{name}\n{hint}") };
assert_eq!(texts, [expected], "only a clipped name needs repeating");
h.step(&json!({"click":{"handle":format!("param:name:{name}")},"double":true})).unwrap();
assert!(h.app.params.as_ref().unwrap().renaming.is_some(), "the name still renames by double-click");
}
}
}
fn escape_dialog() -> crate::scenario::Harness {
let mut h = crate::scenario::Harness::new();
h.app.home.open = false;
+3
View File
@@ -1156,6 +1156,9 @@ const ELEMENTS: &[Element] = &[
element!("text", "dialog:report:gallery", Some("dialog:report:gallery"), |ui, _| {
text(ui, "Validation report with retained units: 10 mm", Text::Report("gallery", false));
}),
element!("name_cell_with_hint", "dialog:cell:Width", Some("dialog:cell:Width"), |ui, _| {
name_cell_with_hint(ui, "Width", false, "Double-click to rename");
}),
element!("search_field", |ui, s| {
search_field(ui, ui.id().with("search"), &mut s.value, "Search");
}),
+17 -1
View File
@@ -678,6 +678,15 @@ pub(crate) fn note_height(ui: &Ui, text: &str) -> f32 {
}
pub(crate) fn text(ui: &mut Ui, text: &str, style: Text<'_>) -> Response {
text_impl(ui, text, style, None)
}
/// One name-cell tooltip: the action hint, with the full name only if the cell clips it.
pub(crate) fn name_cell_with_hint(ui: &mut Ui, name: &str, strong: bool, hint: &str) -> Response {
text_impl(ui, name, Text::NameCell(strong), Some(hint))
}
fn text_impl(ui: &mut Ui, text: &str, style: Text<'_>, hover_hint: Option<&str>) -> Response {
if crate::prefs_search::querying(ui) {
let heading = matches!(style, Text::Heading(_));
if heading {
@@ -718,9 +727,16 @@ pub(crate) fn text(ui: &mut Ui, text: &str, style: Text<'_>) -> Response {
vec2(dimension(ui.available_width()), row_height()),
if matches!(style, Text::Value(_, _)) { egui::Sense::hover() } else { egui::Sense::click() },
);
let tooltip = hover_hint.map(|hint| {
if ui.painter().layout_no_wrap(text.to_owned(), font.clone(), ink).size().x > rect.width() {
format!("{text}\n{hint}")
} else {
hint.to_owned()
}
});
paint_text(ui, rect, text, font, ink, false);
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, ui.is_enabled(), text));
response.on_hover_text(text)
response.on_hover_text(tooltip.as_deref().unwrap_or(text))
} else if matches!(style, Text::Error(_)) {
egui::Frame::NONE
.fill(t.form_error_fill)
+5
View File
@@ -319,6 +319,11 @@ board test captures both strip widths with expanded and collapsed browsers
| `Text::Inline` | Native truncated label; rect/handle `dialog:inline:TEXT` |
| `Text::Cell` | Fixed-height clipped clickable cell with tooltip; rect/handle `dialog:cell:TEXT` |
`name_cell_with_hint(ui, name, strong, hint)` keeps the name cell's allocation and
scenario identity, but owns its single tooltip: the action hint alone when the
name fits, or the full name followed by the hint when it is clipped. Use it instead
of adding a second hover tooltip to `Text::NameCell`.
Examples:
```rust