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 )
	}
}