authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 11:50:52-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 11:56:16-07:00
logd596a5bc8ce82fbf0d90660c5cc24b5b48b16922
tree1116387b6d74fb3ef015ec2b1385da0fc20c4371
parent93933f26a4f155a3654291ce96aa67bfcf3c69f3
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: tooltips centre under their control, shifting only to stay inside the window

A tooltip opened from the leading edge of its control, so a wide one hung off to the right of a toolbar button. It now centres on the control, and near an edge of the window moves only as far as keeps it inside the window's margin. Assisted-by: claude-opus-5.5

4 files changed, 52 insertions(+), 3 deletions(-)

crates/ui/src/layout.rs+1-1
......@@ -62,7 +62,7 @@ fn resolve(nodes: &mut [Built], popup: &[usize], index: usize, states: &HashMap<
6262 let around = match anchor {
6363 Anchor::Point([x, y]) => [x, y, x, y],
6464 Anchor::Dialog | Anchor::Top => [0.0; 4],
65 Anchor::Below(id) | Anchor::Right(id) | Anchor::Over(id) => {
65 Anchor::Below(id) | Anchor::Tip(id) | Anchor::Right(id) | Anchor::Over(id) => {
6666 let (rect, holder) = laid_out(id).unwrap_or_default();
6767 let [left, top, right, bottom] = rect;
6868 match anchor {
crates/ui/src/lib.rs+7
......@@ -230,6 +230,8 @@ pub enum Anchor {
230230 /// Over the box from its corner, the popup's contents covering it, as a combo box opens
231231 /// into its own list; at least as wide as it.
232232 Over(Id),
233 /// Under the box, centred on it, as a tooltip shows.
234 Tip(Id),
233235 /// At a point, as a context menu opens.
234236 Point([f32; 2]),
235237 /// Centred across the window near its top, over the interface, which dims, as a dialog
......@@ -252,6 +254,10 @@ impl Anchor {
252254 Anchor::Below(_) | Anchor::Point(_) => Some(1),
253255 Anchor::Right(_) => Some(0),
254256 Anchor::Over(_) => None,
257 Anchor::Tip(_) if axis == 0 => {
258 return ((around[0] + around[2] - size) / 2.0).clamp(POPUP_MARGIN, most);
259 }
260 Anchor::Tip(_) => Some(1),
255261 Anchor::Dialog | Anchor::Top if axis == 0 => {
256262 return ((room - size) / 2.0).max(POPUP_MARGIN);
257263 }
......@@ -593,6 +599,7 @@ impl Placed {
593599 } = self;
594600 let pivot = match anchor {
595601 Anchor::Below(_) | Anchor::Point(_) => [around[0], around[3]],
602 Anchor::Tip(_) => [(around[0] + around[2]) / 2.0, around[3]],
596603 Anchor::Right(_) => [around[2], around[1]],
597604 Anchor::Over(_) => [around[0], around[1]],
598605 Anchor::Dialog | Anchor::Top => [(rect[0] + rect[2]) / 2.0, rect[1]],
crates/ui/src/popup.rs+2-2
......@@ -220,7 +220,7 @@ pub fn submenus(ui: &mut Ui, id: Id, items: &[Item], submenu: impl Fn(usize) ->
220220}
221221
222222/// Shows `title`, with the `keys` that run it and a `description` under it, in a tooltip
223/// below the box built last while the pointer rests on it or on a box inside it:
223/// centred below the box built last while the pointer rests on it or on a box inside it:
224224/// after a delay, or at once while another has just shown. A press or the wheel hides it
225225/// until the pointer leaves. It names the box to assistive technology, or where the box has
226226/// no role, the unnamed controls inside it.
......@@ -316,7 +316,7 @@ fn tooltip_below(
316316 radius: 4.0,
317317 pad: [8.0, 5.0],
318318 gap: 3.0,
319 anchor: Some(Anchor::Below(part.unwrap_or(id))),
319 anchor: Some(Anchor::Tip(part.unwrap_or(id))),
320320 ..Spec::default()
321321 };
322322 ui.open_as(id.child("tooltip"), spec);
crates/ui/src/tests.rs+42
......@@ -1266,6 +1266,48 @@ fn tooltips_wait_then_switch_at_once_and_hide_on_press_until_left() {
12661266 assert!(rest(&mut ui, 500)[0].is_some());
12671267}
12681268
1269#[test]
1270fn a_tooltip_centres_under_its_control_and_shifts_only_to_stay_in_the_window() {
1271 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1272 let mut shown_at = |x: f32| {
1273 ui.event(Event::PointerMoved([x + 10.0, 110.0]));
1274 let mut tip = None;
1275 for _ in 0..40 {
1276 frame(&mut ui, |ui| {
1277 let id = ui.open(
1278 "tool",
1279 Spec {
1280 flags: Flags::CLICKABLE | Flags::FLOAT,
1281 size: [px(20.0), px(20.0)],
1282 position: [x, 100.0],
1283 ..Spec::default()
1284 },
1285 );
1286 ui.close();
1287 popup::tooltip(ui, "Bold", "Ctrl+B", Some("Makes the selected text bold."));
1288 tip = Some(id.child("tooltip"));
1289 });
1290 }
1291 ui.laid_out(tip.unwrap()).unwrap()
1292 };
1293 let middle = shown_at(190.0);
1294 assert_eq!(middle[1], 120.0 + popup::PAD, "under the control");
1295 assert!(
1296 ((middle[0] + middle[2]) / 2.0 - 200.0).abs() <= 0.5,
1297 "centred on it: {middle:?}"
1298 );
1299 let width = middle[2] - middle[0];
1300 assert_eq!(
1301 shown_at(2.0)[..3],
1302 [POPUP_MARGIN, 120.0 + popup::PAD, POPUP_MARGIN + width]
1303 );
1304 assert_eq!(
1305 shown_at(378.0)[0],
1306 400.0 - POPUP_MARGIN - width,
1307 "shifted only to fit"
1308 );
1309}
1310
12691311#[test]
12701312fn a_popup_opens_beside_a_box_first_laid_out_in_the_same_frame() {
12711313 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);