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.
158 lines
4.8 KiB
Rust
158 lines
4.8 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
use super::app_data::AppData;
|
|
use super::surface::SurfaceFocus;
|
|
use crate::app::App;
|
|
use crate::types::{ Length, Rect };
|
|
|
|
pub const TOOLTIP_DELAY: std::time::Duration = std::time::Duration::from_millis( 600 );
|
|
|
|
#[ derive( Clone ) ]
|
|
pub struct TooltipPending
|
|
{
|
|
pub focus: SurfaceFocus,
|
|
pub flat_idx: usize,
|
|
pub deadline: std::time::Instant,
|
|
pub text: String,
|
|
pub anchor: Rect,
|
|
}
|
|
|
|
#[ derive( Clone ) ]
|
|
pub struct TooltipVisible
|
|
{
|
|
pub focus: SurfaceFocus,
|
|
pub anchor: Rect,
|
|
pub text: String,
|
|
}
|
|
|
|
impl<A: App> AppData<A>
|
|
{
|
|
pub( crate ) fn arm_tooltip( &mut self, focus: SurfaceFocus, flat_idx: usize )
|
|
{
|
|
let Some( ss ) = self.try_surface( focus ) else { return };
|
|
let Some( w ) = crate::tree::find_widget( &ss.frame.widget_rects, flat_idx ) else { return };
|
|
let Some( text ) = w.tooltip.clone() else
|
|
{
|
|
self.tooltip_pending = None;
|
|
return;
|
|
};
|
|
let anchor = w.rect;
|
|
if let Some( ref p ) = self.tooltip_pending
|
|
{
|
|
if p.focus == focus && p.flat_idx == flat_idx { return; }
|
|
}
|
|
self.tooltip_pending = Some( TooltipPending
|
|
{
|
|
focus,
|
|
flat_idx,
|
|
deadline: std::time::Instant::now() + TOOLTIP_DELAY,
|
|
text,
|
|
anchor,
|
|
} );
|
|
if self.tooltip_visible.is_some()
|
|
{
|
|
self.tooltip_visible = None;
|
|
self.overlays_dirty = true;
|
|
}
|
|
}
|
|
|
|
pub( crate ) fn cancel_tooltip( &mut self )
|
|
{
|
|
let was_visible = self.tooltip_visible.take().is_some();
|
|
self.tooltip_pending = None;
|
|
if was_visible { self.overlays_dirty = true; }
|
|
}
|
|
|
|
pub( crate ) fn next_tooltip_wakeup( &self ) -> Option<std::time::Duration>
|
|
{
|
|
let p = self.tooltip_pending.as_ref()?;
|
|
Some( p.deadline.saturating_duration_since( std::time::Instant::now() ) )
|
|
}
|
|
|
|
pub( crate ) fn check_tooltip_deadline( &mut self )
|
|
{
|
|
let Some( p ) = self.tooltip_pending.as_ref() else { return };
|
|
if std::time::Instant::now() < p.deadline { return; }
|
|
let p = self.tooltip_pending.take().unwrap();
|
|
self.tooltip_visible = Some( TooltipVisible
|
|
{
|
|
focus: p.focus,
|
|
anchor: p.anchor,
|
|
text: p.text,
|
|
} );
|
|
self.overlays_dirty = true;
|
|
self.main.request_redraw();
|
|
}
|
|
|
|
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 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 );
|
|
let pill: crate::widget::Element<A::Message> = crate::widget::container(
|
|
crate::widget::text( v.text.clone() )
|
|
.size( 13.0 )
|
|
.color( palette.bg )
|
|
)
|
|
.background( bg_col )
|
|
.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 * 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 - 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();
|
|
|
|
Some( crate::app::OverlaySpec
|
|
{
|
|
id,
|
|
layer: crate::app::Layer::Overlay,
|
|
anchor: crate::app::Anchor::ALL,
|
|
size: ( Length::px( 0.0 ), Length::px( 0.0 ) ),
|
|
exclusive_zone: -1,
|
|
keyboard_exclusive: false,
|
|
input_region: Some( Vec::new() ),
|
|
view,
|
|
on_dismiss: None,
|
|
anchor_widget_id: None,
|
|
} )
|
|
}
|
|
}
|