// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. 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 AppData { 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 { 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> { 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 = 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 = 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, } ) } }