| author | |
| committer | |
| log | d596a5bc8ce82fbf0d90660c5cc24b5b48b16922 |
| tree | 1116387b6d74fb3ef015ec2b1385da0fc20c4371 |
| parent | 93933f26a4f155a3654291ce96aa67bfcf3c69f3 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
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.54 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< |
| 62 | 62 | let around = match anchor { |
| 63 | 63 | Anchor::Point([x, y]) => [x, y, x, y], |
| 64 | 64 | 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) => { | |
| 66 | 66 | let (rect, holder) = laid_out(id).unwrap_or_default(); |
| 67 | 67 | let [left, top, right, bottom] = rect; |
| 68 | 68 | match anchor { |
crates/ui/src/lib.rs+7| ... | ... | @@ -230,6 +230,8 @@ pub enum Anchor { |
| 230 | 230 | /// Over the box from its corner, the popup's contents covering it, as a combo box opens |
| 231 | 231 | /// into its own list; at least as wide as it. |
| 232 | 232 | Over(Id), |
| 233 | /// Under the box, centred on it, as a tooltip shows. | |
| 234 | Tip(Id), | |
| 233 | 235 | /// At a point, as a context menu opens. |
| 234 | 236 | Point([f32; 2]), |
| 235 | 237 | /// Centred across the window near its top, over the interface, which dims, as a dialog |
| ... | ... | @@ -252,6 +254,10 @@ impl Anchor { |
| 252 | 254 | Anchor::Below(_) | Anchor::Point(_) => Some(1), |
| 253 | 255 | Anchor::Right(_) => Some(0), |
| 254 | 256 | 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), | |
| 255 | 261 | Anchor::Dialog | Anchor::Top if axis == 0 => { |
| 256 | 262 | return ((room - size) / 2.0).max(POPUP_MARGIN); |
| 257 | 263 | } |
| ... | ... | @@ -593,6 +599,7 @@ impl Placed { |
| 593 | 599 | } = self; |
| 594 | 600 | let pivot = match anchor { |
| 595 | 601 | Anchor::Below(_) | Anchor::Point(_) => [around[0], around[3]], |
| 602 | Anchor::Tip(_) => [(around[0] + around[2]) / 2.0, around[3]], | |
| 596 | 603 | Anchor::Right(_) => [around[2], around[1]], |
| 597 | 604 | Anchor::Over(_) => [around[0], around[1]], |
| 598 | 605 | 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) -> |
| 220 | 220 | } |
| 221 | 221 | |
| 222 | 222 | /// 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: | |
| 224 | 224 | /// after a delay, or at once while another has just shown. A press or the wheel hides it |
| 225 | 225 | /// until the pointer leaves. It names the box to assistive technology, or where the box has |
| 226 | 226 | /// no role, the unnamed controls inside it. |
| ... | ... | @@ -316,7 +316,7 @@ fn tooltip_below( |
| 316 | 316 | radius: 4.0, |
| 317 | 317 | pad: [8.0, 5.0], |
| 318 | 318 | gap: 3.0, |
| 319 | anchor: Some(Anchor::Below(part.unwrap_or(id))), | |
| 319 | anchor: Some(Anchor::Tip(part.unwrap_or(id))), | |
| 320 | 320 | ..Spec::default() |
| 321 | 321 | }; |
| 322 | 322 | 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() { |
| 1266 | 1266 | assert!(rest(&mut ui, 500)[0].is_some()); |
| 1267 | 1267 | } |
| 1268 | 1268 | |
| 1269 | #[test] | |
| 1270 | fn 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 | ||
| 1269 | 1311 | #[test] |
| 1270 | 1312 | fn a_popup_opens_beside_a_box_first_laid_out_in_the_same_frame() { |
| 1271 | 1313 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); |