tooltip: compute the hover pill in the overlay's physical space, drop it on scale change
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:
@@ -92,6 +92,10 @@ impl<A: App> CompositorHandler for AppData<A>
|
|||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
( pw, ph )
|
( 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
|
// Notify the app of the new physical dimensions. The previous
|
||||||
// `on_resize` it received was scaled with the OLD factor, so any
|
// `on_resize` it received was scaled with the OLD factor, so any
|
||||||
// app-side state keyed off those pixels is now stale. Only fire
|
// app-side state keyed off those pixels is now stale. Only fire
|
||||||
|
|||||||
@@ -88,12 +88,27 @@ impl<A: App> AppData<A>
|
|||||||
pub( crate ) fn tooltip_overlay( &self ) -> Option<crate::app::OverlaySpec<A::Message>>
|
pub( crate ) fn tooltip_overlay( &self ) -> Option<crate::app::OverlaySpec<A::Message>>
|
||||||
{
|
{
|
||||||
let v = self.tooltip_visible.as_ref()?;
|
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 ( ox, oy ) = self.surface_offset_for( v.focus );
|
||||||
let scale = self.try_surface( v.focus )?.scale_factor as f32;
|
let src_scale = self.try_surface( v.focus )?.scale_factor.max( 1 ) as f32;
|
||||||
let anchor_x = ox + v.anchor.x / scale;
|
let anchor_x = ( ox + v.anchor.x / src_scale ) * sf;
|
||||||
let anchor_y = oy + v.anchor.y / scale;
|
let anchor_y = ( oy + v.anchor.y / src_scale ) * sf;
|
||||||
let anchor_w = v.anchor.width / scale;
|
let anchor_w = v.anchor.width / src_scale * sf;
|
||||||
let anchor_h = v.anchor.height / scale;
|
let anchor_h = v.anchor.height / src_scale * sf;
|
||||||
|
|
||||||
let palette = crate::theme::palette();
|
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 );
|
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 )
|
.color( palette.bg )
|
||||||
)
|
)
|
||||||
.background( bg_col )
|
.background( bg_col )
|
||||||
.padding_h( 12.0 )
|
.padding_h( 12.0 * sf )
|
||||||
.padding_v( 6.0 )
|
.padding_v( 6.0 * sf )
|
||||||
.radius( 8.0 )
|
.radius( 8.0 * sf )
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let estimated_w = ( v.text.chars().count() as f32 * 7.5 + 24.0 ).clamp( 40.0, 320.0 );
|
let estimated_w = ( v.text.chars().count() as f32 * 7.5 * ts * sf + 24.0 * sf )
|
||||||
let estimated_h = 28.0_f32;
|
.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 x = anchor_x + ( anchor_w - estimated_w ) / 2.0;
|
||||||
let mut y = anchor_y - estimated_h - 6.0;
|
let mut y = anchor_y - estimated_h - gap;
|
||||||
let sw = self.main.width as f32;
|
let sw = self.main.width as f32 * sf;
|
||||||
let sh = self.main.height as f32;
|
let sh = self.main.height as f32 * sf;
|
||||||
x = x.clamp( 4.0, ( sw - estimated_w - 4.0 ).max( 4.0 ) );
|
x = x.clamp( margin, ( sw - estimated_w - margin ).max( margin ) );
|
||||||
if y < 4.0 { y = anchor_y + anchor_h + 6.0; }
|
if y < margin { y = anchor_y + anchor_h + gap; }
|
||||||
if y + estimated_h > sh - 4.0 { y = ( sh - estimated_h - 4.0 ).max( 4.0 ); }
|
if y + estimated_h > sh - margin { y = ( sh - estimated_h - margin ).max( margin ); }
|
||||||
|
|
||||||
let view: crate::widget::Element<A::Message> = crate::layout::stack::stack()
|
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 )
|
.push_translated( pill, crate::layout::stack::HAlign::Start, crate::layout::stack::VAlign::Top, x, y )
|
||||||
.into();
|
.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
|
Some( crate::app::OverlaySpec
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
|
|||||||
Reference in New Issue
Block a user