tooltip: compute the hover pill in the overlay's physical space, drop it on scale change
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Layout runs in the physical pixels of each surface's buffer, but tooltip_overlay positioned the pill in logical ones: the anchor was divided by the source surface's scale and clamped against the logical screen size, and the resulting x/y were then applied verbatim as a physical stack translation. At scale 1 the two spaces coincide and the mix is invisible; at scale 2 the tooltip painted at half its position, towards the top-left corner and away from its icon. The whole computation now lives in the overlay's physical space: the anchor converts logical → physical with the tooltip overlay's own scale (falling back to the main surface's before the overlay exists), and the pill's estimated size folds in that scale plus the accessibility text_scale, which the text raster path already applied but the estimate ignored — the reason centring and edge clamping drifted even at scale 1 on long labels. Padding, radius, the gap above the anchor and the screen margins scale along, so the pill no longer renders with shrunken chrome around full-size text on scaled outputs.
scale_factor_changed additionally cancels any pending or visible tooltip — its anchor rect was captured in the previous scale's physical pixels and nothing invalidated it; hovering re-arms it at the now-correct position — and marks overlays_dirty so overlay specs rebuild with the new scale instead of keeping geometry baked under the old one.
This commit is contained in:
2026-08-11 15:40:23 +02:00
parent 477ef13ff4
commit 98c82385c7
2 changed files with 38 additions and 21 deletions

View File

@@ -92,6 +92,10 @@ impl<A: App> CompositorHandler for AppData<A>
ss.request_redraw();
( pw, ph )
};
// Anchors and baked overlay geometry are in the old scale's
// physical pixels — drop the tooltip and rebuild the specs.
self.cancel_tooltip();
self.overlays_dirty = true;
// Notify the app of the new physical dimensions. The previous
// `on_resize` it received was scaled with the OLD factor, so any
// app-side state keyed off those pixels is now stale. Only fire

View File

@@ -88,12 +88,27 @@ impl<A: App> AppData<A>
pub( crate ) fn tooltip_overlay( &self ) -> Option<crate::app::OverlaySpec<A::Message>>
{
let v = self.tooltip_visible.as_ref()?;
use std::hash::{ Hash, Hasher };
let mut hasher = std::collections::hash_map::DefaultHasher::new();
"ltk-tooltip".hash( &mut hasher );
let id = crate::app::OverlayId( hasher.finish() as u32 );
// Layout runs in the overlay's physical buffer space, so every
// coordinate below is physical. The overlay's own scale wins once
// the surface exists; before that, main's is the best predictor.
let sf = self.overlays.get( &id )
.map( |ss| ss.scale_factor )
.unwrap_or( self.main.scale_factor )
.max( 1 ) as f32;
let ts = crate::types::text_scale();
let ( ox, oy ) = self.surface_offset_for( v.focus );
let scale = self.try_surface( v.focus )?.scale_factor as f32;
let anchor_x = ox + v.anchor.x / scale;
let anchor_y = oy + v.anchor.y / scale;
let anchor_w = v.anchor.width / scale;
let anchor_h = v.anchor.height / scale;
let src_scale = self.try_surface( v.focus )?.scale_factor.max( 1 ) as f32;
let anchor_x = ( ox + v.anchor.x / src_scale ) * sf;
let anchor_y = ( oy + v.anchor.y / src_scale ) * sf;
let anchor_w = v.anchor.width / src_scale * sf;
let anchor_h = v.anchor.height / src_scale * sf;
let palette = crate::theme::palette();
let bg_col = crate::types::Color::rgba( palette.text_primary.r, palette.text_primary.g, palette.text_primary.b, 0.95 );
@@ -103,30 +118,28 @@ impl<A: App> AppData<A>
.color( palette.bg )
)
.background( bg_col )
.padding_h( 12.0 )
.padding_v( 6.0 )
.radius( 8.0 )
.padding_h( 12.0 * sf )
.padding_v( 6.0 * sf )
.radius( 8.0 * sf )
.into();
let estimated_w = ( v.text.chars().count() as f32 * 7.5 + 24.0 ).clamp( 40.0, 320.0 );
let estimated_h = 28.0_f32;
let estimated_w = ( v.text.chars().count() as f32 * 7.5 * ts * sf + 24.0 * sf )
.clamp( 40.0 * sf, 320.0 * sf );
let estimated_h = 16.0 * ts * sf + 12.0 * sf;
let gap = 6.0 * sf;
let margin = 4.0 * sf;
let mut x = anchor_x + ( anchor_w - estimated_w ) / 2.0;
let mut y = anchor_y - estimated_h - 6.0;
let sw = self.main.width as f32;
let sh = self.main.height as f32;
x = x.clamp( 4.0, ( sw - estimated_w - 4.0 ).max( 4.0 ) );
if y < 4.0 { y = anchor_y + anchor_h + 6.0; }
if y + estimated_h > sh - 4.0 { y = ( sh - estimated_h - 4.0 ).max( 4.0 ); }
let mut y = anchor_y - estimated_h - gap;
let sw = self.main.width as f32 * sf;
let sh = self.main.height as f32 * sf;
x = x.clamp( margin, ( sw - estimated_w - margin ).max( margin ) );
if y < margin { y = anchor_y + anchor_h + gap; }
if y + estimated_h > sh - margin { y = ( sh - estimated_h - margin ).max( margin ); }
let view: crate::widget::Element<A::Message> = crate::layout::stack::stack()
.push_translated( pill, crate::layout::stack::HAlign::Start, crate::layout::stack::VAlign::Top, x, y )
.into();
use std::hash::{ Hash, Hasher };
let mut hasher = std::collections::hash_map::DefaultHasher::new();
"ltk-tooltip".hash( &mut hasher );
let id = crate::app::OverlayId( hasher.finish() as u32 );
Some( crate::app::OverlaySpec
{
id,