ltk/widget/pressable/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::render::Canvas;
use crate::types::WidgetId;
use super::Element;
#[ cfg( test ) ]
mod tests;
/// Wraps any [`Element`] and emits a message on tap. Use when you want
/// click-to-emit on something richer than a [`Button`](super::button::Button)
/// — for example a [`Container`](super::container::Container) styled as a
/// card holding a row of icon + labels.
///
/// The wrapper is invisible to drawing: it delegates `preferred_size` and
/// rendering to the child. It does record a hit rect covering its full
/// allocated rect so taps anywhere inside fire `on_press`. Inner widgets
/// that are themselves interactive (e.g. a button nested inside the
/// pressable) keep priority — the layout pass pushes the wrapper's hit
/// rect *before* recursing into the child, and hit testing iterates in
/// reverse, so deeper widgets win.
///
/// No visual press feedback is applied — for state-driven appearance
/// changes use a [`Button`](super::button::Button) or compose with a
/// container that reacts to focus/press signals.
///
/// ```rust,no_run
/// # use ltk::{ column, container, pressable, row, Element, Pressable };
/// # #[ derive( Clone ) ] enum Msg { OpenWifiPicker }
/// # fn _ex(
/// # icon: Element<Msg>,
/// # title: Element<Msg>,
/// # subtitle: Element<Msg>,
/// # ) -> Pressable<Msg> {
/// pressable(
/// container( row()
/// .push( icon )
/// .push( column().push( title ).push( subtitle ) ) )
/// .surface( "surface-card" )
/// .radius( 32.0 )
/// .padding_h( 16.5 )
/// .padding_v( 24.0 ),
/// )
/// .on_press( Msg::OpenWifiPicker )
/// # }
/// ```
pub struct Pressable<Msg: Clone>
{
pub child: Box<Element<Msg>>,
pub on_press: Option<Msg>,
pub on_long_press: Option<Msg>,
/// Drag-arm message. Fired alongside `on_long_press` when the touch
/// hold timer elapses, AND fired by mouse left-button motion past
/// the drag-promotion threshold without waiting for the timer. The
/// caller uses this to arm any per-app drag state (in crustace,
/// `dragging_item`); a widget that opens a context menu but isn't
/// draggable leaves this `None`.
pub on_drag_start: Option<Msg>,
/// Keyboard `Escape`-key message. The runtime scans every laid-out
/// pressable's snapshot and fires the topmost (highest `flat_idx`)
/// `on_escape` it finds before the default ESC fallthrough chain.
/// Used by [`crate::widget::dialog::Dialog`] to make `Esc` cancel a
/// modal dialog without each app having to wire a global keyboard
/// hook — but available to any composite that needs the same
/// semantics.
pub on_escape: Option<Msg>,
/// Make the pressable hit-testable even when no callback is set.
/// A `swallow=true` pressable consumes pointer events at its hit
/// rect and emits no message — used by
/// [`crate::widget::dialog::Dialog`] for the modal scrim plus the
/// card-area swallow that prevents `dismiss_on_scrim` from firing
/// when the user clicks on the dialog body itself. Has no effect
/// when any handler is also set; in that case the handler determines
/// the message and `swallow` is implicit.
pub swallow: bool,
pub id: Option<WidgetId>,
pub cursor: Option<crate::types::CursorShape>,
}
impl<Msg: Clone> Pressable<Msg>
{
pub fn new( child: impl Into<Element<Msg>> ) -> Self
{
Self
{
child: Box::new( child.into() ),
on_press: None,
on_long_press: None,
on_drag_start: None,
on_escape: None,
swallow: false,
id: None,
cursor: None,
}
}
/// Override the pointer cursor shape shown on hover.
pub fn cursor( mut self, shape: crate::types::CursorShape ) -> Self
{
self.cursor = Some( shape );
self
}
pub fn on_press( mut self, msg: Msg ) -> Self
{
self.on_press = Some( msg );
self
}
pub fn on_long_press( mut self, msg: Msg ) -> Self
{
self.on_long_press = Some( msg );
self
}
/// Attach a drag-arm message. Fires when the press transitions into
/// drag mode — touch on hold-timer expiry, mouse on motion past the
/// drag-promotion threshold. Independent of `on_long_press` so a
/// widget can open a menu without becoming draggable, or be
/// draggable without opening a menu.
pub fn on_drag_start( mut self, msg: Msg ) -> Self
{
self.on_drag_start = Some( msg );
self
}
/// Bind a keyboard-`Escape` message to this pressable. See
/// [`Pressable::on_escape`] for the dispatch order semantics.
pub fn on_escape( mut self, msg: Msg ) -> Self
{
self.on_escape = Some( msg );
self
}
/// Make the pressable hit-testable even when no `on_press` /
/// `on_long_press` / `on_drag_start` is configured. See
/// [`Pressable::swallow`].
pub fn swallow( mut self, on: bool ) -> Self
{
self.swallow = on;
self
}
pub fn id( mut self, id: WidgetId ) -> Self
{
self.id = Some( id );
self
}
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
{
self.child.preferred_size( max_width, canvas )
}
/// True when the wrapper participates in hit-testing — has at least
/// one pointer / keyboard handler set, or has been opted in to
/// silent swallow via [`Pressable::swallow`]. Used by the layout
/// pass to skip pushing a hit rect for a no-op pressable.
pub fn has_handler( &self ) -> bool
{
self.on_press.is_some()
|| self.on_long_press.is_some()
|| self.on_drag_start.is_some()
|| self.on_escape.is_some()
|| self.swallow
}
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Pressable<U>
where
U: Clone + 'static,
Msg: 'static,
{
Pressable
{
child: Box::new( self.child.map_arc( f ) ),
on_press: self.on_press.map( |m| ( *f )( m ) ),
on_long_press: self.on_long_press.map( |m| ( *f )( m ) ),
on_drag_start: self.on_drag_start.map( |m| ( *f )( m ) ),
on_escape: self.on_escape.map( |m| ( *f )( m ) ),
swallow: self.swallow,
id: self.id,
cursor: self.cursor,
}
}
}
pub fn pressable<Msg: Clone>( child: impl Into<Element<Msg>> ) -> Pressable<Msg>
{
Pressable::new( child )
}
impl<Msg: Clone + 'static> From<Pressable<Msg>> for Element<Msg>
{
fn from( p: Pressable<Msg> ) -> Self
{
Element::Pressable( p )
}
}