event_loop, widget, input: pointer-dwell tooltips, global drag coords, foreign-toplevel name cascade
`Button::tooltip( text )` registers a hint string that fires after a 600 ms pointer dwell. `LaidOutWidget` gains a `tooltip: Option<String>` field, `Element::tooltip()` exposes it to the input layer, and the existing pointer-hover path now calls `arm_tooltip` on hover-enter and `cancel_tooltip` on hover-leave / touch. The deadline is polled alongside `next_long_press_wakeup` in `try_run` so an idle pointer still gets a wake-up at the firing instant; on fire, `tooltip_overlay()` synthesises an `OverlaySpec` — a rounded `text_primary @ 95%` pill drawn with `bg` text — anchored above the hovered widget, flipping below or clamping inside the screen if it would clip, and pushed alongside the app's own overlays both in the redraw path and in `reconcile_overlays` so the layer surface is created the same frame the tooltip becomes visible. Pointer-only by design: touch events explicitly cancel because a tap-and-release should never linger into a hint. The `showcase` example wires `.tooltip(..)` on the three button variants as a smoke test. The drag pipeline now reports positions in main-surface (global) coordinates instead of per-surface. `surface_offset_for( focus )` derives the top-left of an overlay surface from `SurfaceState::layer_anchor` — newly stored at `reconcile_overlays` time from `OverlaySpec::anchor` — combined with the main surface's dimensions; `on_drag_move`, `on_drop`, the synthetic move emitted on drag-promotion in `pointer.rs`, and the `pending_drag_inits` push site in `gesture::start_drag` all translate before handing coordinates to the app. The motion and release paths additionally `request_redraw()` every overlay so a dock-style drop target painted on an `Anchor::Bottom` layer surface gets repainted as the drag moves — without that, the visible drop indicator only updates when the cursor re-enters the main surface. Drops still target whichever surface fired the release; only the coordinates are unified. `ForeignToplevelListHandler` previously read `app_id` directly via `ForeignToplevelList::info()`. Clients that never set `app_id` (some winit-windowed compositors, simple test clients) were silently invisible to crustace-style docks because the empty string fell through `unwrap_or_default()` and the dock then keyed entries off `""`. `toplevel_display_id()` cascades: prefer `app_id` for desktop-entry matching, fall back to `title` for human-readable identification, and finally to the protocol-issued `identifier` which is always present and unique per handle. Applied to both `new_toplevel` and `update_toplevel`. `theme::system_fontdb()` lazily loads the system font database once via `OnceLock` and reuses the `Arc` for every `decode_svg_bytes` call. resvg's default `Options::fontdb` is empty, so any SVG containing `<text>` rendered with the built-in fallback font or no font at all; with the system DB attached, icons and decorative SVGs with embedded labels now resolve glyphs correctly. Cached because `load_system_fonts()` walks every font path on the system and is comfortably tens of milliseconds on a cold cache — not something to repeat per icon decode. `themes/default/theme.json` tweaks one variant's slot palette: `surface-alt` from `@indigo/D9` to `@white/D9` and `text-primary` from `@white` to `@navy`, plus a cosmetic re-alignment of the `"value"` columns in the same slots block.
This commit is contained in:
@@ -239,6 +239,26 @@ pub( crate ) enum SurfaceFocus
|
||||
Overlay( OverlayId ),
|
||||
}
|
||||
|
||||
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: crate::types::Rect,
|
||||
}
|
||||
|
||||
#[derive( Clone )]
|
||||
pub struct TooltipVisible
|
||||
{
|
||||
pub focus: SurfaceFocus,
|
||||
pub anchor: crate::types::Rect,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Configuration for a layer-shell surface, used both for the main surface
|
||||
/// (when the app uses [`crate::app::ShellMode::Layer`]) and for each overlay
|
||||
/// returned by [`crate::app::App::overlays`].
|
||||
@@ -415,6 +435,7 @@ pub( crate ) struct SurfaceState<Msg: Clone>
|
||||
/// for slide-down / grow animations). `(0, 0)` for non-layer surfaces
|
||||
/// and for layer surfaces before their first configure.
|
||||
pub last_requested_size: ( u32, u32 ),
|
||||
pub layer_anchor: Option<crate::app::Anchor>,
|
||||
/// Anchor rect the xdg-popup positioner was last configured with.
|
||||
/// `None` for non-popup surfaces.
|
||||
pub last_popup_anchor: Option<Rect>,
|
||||
@@ -465,6 +486,7 @@ impl<Msg: Clone> SurfaceState<Msg>
|
||||
titlebar_close_rect: Rect::default(),
|
||||
scale_factor: 1,
|
||||
last_requested_size: ( 0, 0 ),
|
||||
layer_anchor: None,
|
||||
last_popup_anchor: None,
|
||||
popup_reposition_token: 0,
|
||||
frame_pending: false,
|
||||
@@ -674,6 +696,9 @@ pub struct AppData<A: App>
|
||||
/// before the user's finger / cursor moves — useful on mouse where the
|
||||
/// pointer might sit perfectly still between press and drag.
|
||||
pub pending_drag_inits: Vec<Point>,
|
||||
|
||||
pub tooltip_pending: Option<TooltipPending>,
|
||||
pub tooltip_visible: Option<TooltipVisible>,
|
||||
pub qh: QueueHandle<Self>,
|
||||
/// Last pointer serial (needed for interactive move).
|
||||
pub last_pointer_serial: u32,
|
||||
@@ -1894,6 +1919,120 @@ impl<A: App> AppData<A>
|
||||
soonest
|
||||
}
|
||||
|
||||
pub( crate ) fn arm_tooltip( &mut self, focus: SurfaceFocus, flat_idx: usize )
|
||||
{
|
||||
let ss = self.surface( focus );
|
||||
let Some( w ) = crate::tree::find_widget( &ss.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()?;
|
||||
let ( ox, oy ) = self.surface_offset_for( v.focus );
|
||||
let scale = self.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 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 )
|
||||
.padding_v( 6.0 )
|
||||
.radius( 8.0 )
|
||||
.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 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 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,
|
||||
layer: crate::app::Layer::Overlay,
|
||||
anchor: crate::app::Anchor::ALL,
|
||||
size: ( 0, 0 ),
|
||||
exclusive_zone: -1,
|
||||
keyboard_exclusive: false,
|
||||
input_region: Some( Vec::new() ),
|
||||
view,
|
||||
on_dismiss: None,
|
||||
anchor_widget_id: None,
|
||||
} )
|
||||
}
|
||||
|
||||
/// Fire long-press messages for any surface whose deadline has
|
||||
/// elapsed. Idempotent — if a press is in-flight but not yet due this
|
||||
/// does nothing. On fire, the message is pushed to `pending_msgs`,
|
||||
@@ -1960,7 +2099,9 @@ impl<A: App> AppData<A>
|
||||
if let Some( m ) = ds_msg
|
||||
{
|
||||
self.pending_msgs.push( m );
|
||||
self.pending_drag_inits.push( origin );
|
||||
let ( ox, oy ) = self.surface_offset_for( focus );
|
||||
let global = crate::Point { x: origin.x + ox, y: origin.y + oy };
|
||||
self.pending_drag_inits.push( global );
|
||||
// Drag promotion cancels any held-button repeat — the
|
||||
// gesture has switched semantics and the timer has
|
||||
// nothing to fire against any more.
|
||||
|
||||
Reference in New Issue
Block a user