pub struct ClickTracker { /* private fields */ }Expand description
Tracks click timing and position for double-click detection.
ClickTracker monitors mouse clicks and determines if a click is a double-click
based on time interval and position proximity.
§Examples
use minui::input::ClickTracker;
let mut tracker = ClickTracker::new();
// Check if a click is a double-click
if tracker.is_double_click(10, 5) {
println!("Double-click detected at (10, 5)");
}Implementations§
Source§impl ClickTracker
impl ClickTracker
Sourcepub fn new() -> Self
pub fn new() -> Self
Creates a new click tracker with default thresholds.
Defaults:
- 500ms double-click threshold
- 3 cell maximum distance
Examples found in repository?
examples/input_demo.rs (line 34)
23fn main() -> minui::Result<()> {
24 let initial_state = InputDemoState {
25 event_log: {
26 let mut log = VecDeque::new();
27 log.push_back("Welcome to MinUI Input Demo!".to_string());
28 log.push_back("Try typing, moving mouse, clicking...".to_string());
29 log.push_back("Double-click quickly to see double-click detection!".to_string());
30 log.push_back("Press 'q' to quit".to_string());
31 log
32 },
33 mouse_pos: (0, 0),
34 click_tracker: ClickTracker::new(),
35 };
36
37 let mut app = App::new(initial_state)?.with_frame_rate(Duration::from_millis(16));
38
39 app.run(
40 |state, event| {
41 // Return false to exit (supports modifier-aware and legacy events).
42 if let Event::KeyWithModifiers(k) = event {
43 if matches!(k.key, KeyKind::Char('q')) {
44 return false;
45 }
46 }
47 if matches!(event, Event::Character('q')) {
48 return false;
49 }
50
51 // Handle events and update state
52 match event {
53 // Prefer modifier-aware keyboard events (the keyboard handler may emit these for most keys).
54 Event::KeyWithModifiers(k) => match k.key {
55 KeyKind::Char(c) => {
56 state.event_log.push_back(format!(
57 "Key: '{}' (mods: shift={}, ctrl={}, alt={}, super={})",
58 c, k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
59 ));
60 }
61 KeyKind::Up => {
62 state.event_log.push_back(format!(
63 "Key: ↑ Up (mods: shift={}, ctrl={}, alt={}, super={})",
64 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
65 ));
66 }
67 KeyKind::Down => {
68 state.event_log.push_back(format!(
69 "Key: ↓ Down (mods: shift={}, ctrl={}, alt={}, super={})",
70 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
71 ));
72 }
73 KeyKind::Left => {
74 state.event_log.push_back(format!(
75 "Key: ← Left (mods: shift={}, ctrl={}, alt={}, super={})",
76 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
77 ));
78 }
79 KeyKind::Right => {
80 state.event_log.push_back(format!(
81 "Key: → Right (mods: shift={}, ctrl={}, alt={}, super={})",
82 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
83 ));
84 }
85 KeyKind::Enter => {
86 state.event_log.push_back(format!(
87 "Key: ⏎ Enter (mods: shift={}, ctrl={}, alt={}, super={})",
88 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
89 ));
90 }
91 KeyKind::Escape => {
92 state.event_log.push_back(format!(
93 "Key: Escape (mods: shift={}, ctrl={}, alt={}, super={})",
94 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
95 ));
96 }
97 KeyKind::Backspace => {
98 state.event_log.push_back(format!(
99 "Key: ⌫ Backspace (mods: shift={}, ctrl={}, alt={}, super={})",
100 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
101 ));
102 }
103 KeyKind::Delete => {
104 state.event_log.push_back(format!(
105 "Key: ⌦ Delete (mods: shift={}, ctrl={}, alt={}, super={})",
106 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
107 ));
108 }
109 KeyKind::Tab => {
110 state.event_log.push_back(format!(
111 "Key: Tab (mods: shift={}, ctrl={}, alt={}, super={})",
112 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
113 ));
114 }
115 KeyKind::Function(n) => {
116 state.event_log.push_back(format!(
117 "Key: F{} (mods: shift={}, ctrl={}, alt={}, super={})",
118 n, k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
119 ));
120 }
121 KeyKind::CapsLock => {
122 state.event_log.push_back("Key: Caps Lock".to_string());
123 }
124 },
125
126 // Legacy fallback keyboard events (some backends may still emit these).
127 Event::Character(c) => {
128 state.event_log.push_back(format!("Key: '{}'", c));
129 }
130 Event::Paste(text) => {
131 // Keep the log readable for large pastes
132 let preview: String = text.chars().take(60).collect();
133 if text.chars().count() > 60 {
134 state.event_log.push_back(format!(
135 "Paste: \"{}…\" ({} chars)",
136 preview,
137 text.chars().count()
138 ));
139 } else {
140 state.event_log.push_back(format!("Paste: \"{}\"", preview));
141 }
142 }
143 Event::KeyUp => {
144 state.event_log.push_back("Key: ↑ Up".to_string());
145 }
146 Event::KeyDown => {
147 state.event_log.push_back("Key: ↓ Down".to_string());
148 }
149 Event::KeyLeft => {
150 state.event_log.push_back("Key: ← Left".to_string());
151 }
152 Event::KeyRight => {
153 state.event_log.push_back("Key: → Right".to_string());
154 }
155 Event::Enter => {
156 state.event_log.push_back("Key: ⏎ Enter".to_string());
157 }
158 Event::Escape => {
159 state.event_log.push_back("Key: Escape".to_string());
160 }
161 Event::Backspace => {
162 state.event_log.push_back("Key: ⌫ Backspace".to_string());
163 }
164 Event::Delete => {
165 state.event_log.push_back("Key: ⌦ Delete".to_string());
166 }
167 Event::FunctionKey(n) => {
168 state.event_log.push_back(format!("Key: F{}", n));
169 }
170
171 // Handle mouse events
172 Event::MouseMove { x, y } => {
173 state.mouse_pos = (x, y);
174 // Only log occasional moves to avoid spam
175 if x % 3 == 0 && y % 3 == 0 {
176 state
177 .event_log
178 .push_back(format!("Mouse: Moved to ({}, {})", x, y));
179 }
180 }
181 Event::MouseClick { x, y, button } => {
182 state.mouse_pos = (x, y);
183 let button_name = match button {
184 MouseButton::Left => "Left",
185 MouseButton::Right => "Right",
186 MouseButton::Middle => "Middle",
187 MouseButton::Other(_) => "Other",
188 };
189
190 // Check for double-click
191 if state.click_tracker.is_double_click(x, y) {
192 state.event_log.push_back(format!(
193 "Mouse: DOUBLE-CLICK! {} button at ({}, {})",
194 button_name, x, y
195 ));
196 } else {
197 state
198 .event_log
199 .push_back(format!("Mouse: {} click at ({}, {})", button_name, x, y));
200 }
201 }
202 Event::MouseDrag { x, y, button } => {
203 state.mouse_pos = (x, y);
204 let button_name = match button {
205 MouseButton::Left => "Left",
206 MouseButton::Right => "Right",
207 MouseButton::Middle => "Middle",
208 MouseButton::Other(_) => "Other",
209 };
210 state
211 .event_log
212 .push_back(format!("Mouse: {} drag to ({}, {})", button_name, x, y));
213 }
214 Event::MouseScroll { delta } => {
215 let direction = if delta > 0 { "up" } else { "down" };
216 state
217 .event_log
218 .push_back(format!("Mouse: Scroll {} ({})", direction, delta));
219 }
220 Event::MouseScrollHorizontal { delta } => {
221 let direction = if delta > 0 { "right" } else { "left" };
222 state
223 .event_log
224 .push_back(format!("Mouse: Scroll {} ({})", direction, delta));
225 }
226 Event::MouseRelease { x, y, button } => {
227 state.mouse_pos = (x, y);
228 let button_name = match button {
229 MouseButton::Left => "Left",
230 MouseButton::Right => "Right",
231 MouseButton::Middle => "Middle",
232 MouseButton::Other(_) => "Other",
233 };
234 state
235 .event_log
236 .push_back(format!("Mouse: {} release at ({}, {})", button_name, x, y));
237 }
238
239 Event::Resize { width, height } => {
240 state
241 .event_log
242 .push_back(format!("Terminal: Resized to {}x{}", width, height));
243 }
244 _ => {}
245 }
246
247 // Keep the log at reasonable size
248 if state.event_log.len() > MAX_EVENTS {
249 state.event_log.pop_front();
250 }
251
252 true
253 },
254 |state, window| {
255 let (term_width, term_height) = window.get_size();
256
257 // Create a container to display the events.
258 //
259 // Panel has been absorbed into Container: use borders + title + padding, and put
260 // content widgets inside as children.
261 // NOTE: `ContainerPadding` is the name exported by the prelude for Container's padding type.
262 // (The underlying type in `container.rs` is `Padding`.)
263 use minui::widgets::ContainerPadding;
264
265 let panel_x: u16 = 2u16;
266 let panel_y: u16 = 1u16;
267 let panel_w: u16 = term_width.saturating_sub(4u16);
268 let panel_h: u16 = term_height.saturating_sub(4u16);
269
270 // Render the log as stacked labels so each event appears on its own line.
271 // We display the newest entries at the top (reverse chronological).
272 let mut log_container = Container::vertical().with_row_gap(Gap::Pixels(0u16));
273 if state.event_log.is_empty() {
274 log_container = log_container.add_child(Label::new("No events yet..."));
275 } else {
276 for line in state.event_log.iter().rev().take(MAX_EVENTS) {
277 log_container = log_container.add_child(Label::new(line.clone()));
278 }
279 }
280
281 let panel = Container::new()
282 .with_position_and_size(panel_x, panel_y, panel_w, panel_h)
283 .with_border()
284 .with_border_chars(BorderChars::double_line())
285 .with_border_color(ColorPair::new(Color::Cyan, Color::Black))
286 .with_title("MinUI Input Demo")
287 .with_title_alignment(TitleAlignment::Center)
288 .with_padding(ContainerPadding::uniform(1u16))
289 .add_child(log_container);
290
291 panel.draw(window)?;
292
293 // Draw mouse position info at bottom
294 let mouse_info = format!("Mouse: ({}, {})", state.mouse_pos.0, state.mouse_pos.1);
295 let info_y = term_height.saturating_sub(2);
296 window.write_str_colored(
297 info_y,
298 2,
299 &mouse_info,
300 ColorPair::new(Color::Cyan, Color::Transparent),
301 )?;
302
303 // Draw instructions at the very bottom
304 let help_text = "Press 'q' to quit | Try typing, clicking, scrolling!";
305 let help_x = (term_width.saturating_sub(help_text.len() as u16)) / 2;
306 let help_y = term_height.saturating_sub(1);
307 window.write_str_colored(
308 help_y,
309 help_x,
310 help_text,
311 ColorPair::new(Color::DarkGray, Color::Transparent),
312 )?;
313
314 window.flush()?;
315 Ok(())
316 },
317 )?;
318
319 Ok(())
320}Sourcepub fn with_threshold(self, threshold: Duration) -> Self
pub fn with_threshold(self, threshold: Duration) -> Self
Sets the maximum time between clicks for double-click detection.
Sourcepub fn with_distance(self, distance: u16) -> Self
pub fn with_distance(self, distance: u16) -> Self
Sets the maximum distance between clicks for double-click detection.
Sourcepub fn is_double_click(&mut self, x: u16, y: u16) -> bool
pub fn is_double_click(&mut self, x: u16, y: u16) -> bool
Checks if a click at the given position is a double-click.
A double-click is detected if:
- The time since the last click is less than
double_click_threshold - The click position is within
double_click_distanceof the last click
§Returns
trueif this is a double-clickfalseotherwise
§Examples
use minui::input::ClickTracker;
let mut tracker = ClickTracker::new();
// First click
assert!(!tracker.is_double_click(10, 5)); // First click is never a double-click
// Simulate a quick second click
// (in real code, you'd call this from the mouse event handler)Examples found in repository?
examples/input_demo.rs (line 191)
23fn main() -> minui::Result<()> {
24 let initial_state = InputDemoState {
25 event_log: {
26 let mut log = VecDeque::new();
27 log.push_back("Welcome to MinUI Input Demo!".to_string());
28 log.push_back("Try typing, moving mouse, clicking...".to_string());
29 log.push_back("Double-click quickly to see double-click detection!".to_string());
30 log.push_back("Press 'q' to quit".to_string());
31 log
32 },
33 mouse_pos: (0, 0),
34 click_tracker: ClickTracker::new(),
35 };
36
37 let mut app = App::new(initial_state)?.with_frame_rate(Duration::from_millis(16));
38
39 app.run(
40 |state, event| {
41 // Return false to exit (supports modifier-aware and legacy events).
42 if let Event::KeyWithModifiers(k) = event {
43 if matches!(k.key, KeyKind::Char('q')) {
44 return false;
45 }
46 }
47 if matches!(event, Event::Character('q')) {
48 return false;
49 }
50
51 // Handle events and update state
52 match event {
53 // Prefer modifier-aware keyboard events (the keyboard handler may emit these for most keys).
54 Event::KeyWithModifiers(k) => match k.key {
55 KeyKind::Char(c) => {
56 state.event_log.push_back(format!(
57 "Key: '{}' (mods: shift={}, ctrl={}, alt={}, super={})",
58 c, k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
59 ));
60 }
61 KeyKind::Up => {
62 state.event_log.push_back(format!(
63 "Key: ↑ Up (mods: shift={}, ctrl={}, alt={}, super={})",
64 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
65 ));
66 }
67 KeyKind::Down => {
68 state.event_log.push_back(format!(
69 "Key: ↓ Down (mods: shift={}, ctrl={}, alt={}, super={})",
70 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
71 ));
72 }
73 KeyKind::Left => {
74 state.event_log.push_back(format!(
75 "Key: ← Left (mods: shift={}, ctrl={}, alt={}, super={})",
76 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
77 ));
78 }
79 KeyKind::Right => {
80 state.event_log.push_back(format!(
81 "Key: → Right (mods: shift={}, ctrl={}, alt={}, super={})",
82 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
83 ));
84 }
85 KeyKind::Enter => {
86 state.event_log.push_back(format!(
87 "Key: ⏎ Enter (mods: shift={}, ctrl={}, alt={}, super={})",
88 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
89 ));
90 }
91 KeyKind::Escape => {
92 state.event_log.push_back(format!(
93 "Key: Escape (mods: shift={}, ctrl={}, alt={}, super={})",
94 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
95 ));
96 }
97 KeyKind::Backspace => {
98 state.event_log.push_back(format!(
99 "Key: ⌫ Backspace (mods: shift={}, ctrl={}, alt={}, super={})",
100 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
101 ));
102 }
103 KeyKind::Delete => {
104 state.event_log.push_back(format!(
105 "Key: ⌦ Delete (mods: shift={}, ctrl={}, alt={}, super={})",
106 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
107 ));
108 }
109 KeyKind::Tab => {
110 state.event_log.push_back(format!(
111 "Key: Tab (mods: shift={}, ctrl={}, alt={}, super={})",
112 k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
113 ));
114 }
115 KeyKind::Function(n) => {
116 state.event_log.push_back(format!(
117 "Key: F{} (mods: shift={}, ctrl={}, alt={}, super={})",
118 n, k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
119 ));
120 }
121 KeyKind::CapsLock => {
122 state.event_log.push_back("Key: Caps Lock".to_string());
123 }
124 },
125
126 // Legacy fallback keyboard events (some backends may still emit these).
127 Event::Character(c) => {
128 state.event_log.push_back(format!("Key: '{}'", c));
129 }
130 Event::Paste(text) => {
131 // Keep the log readable for large pastes
132 let preview: String = text.chars().take(60).collect();
133 if text.chars().count() > 60 {
134 state.event_log.push_back(format!(
135 "Paste: \"{}…\" ({} chars)",
136 preview,
137 text.chars().count()
138 ));
139 } else {
140 state.event_log.push_back(format!("Paste: \"{}\"", preview));
141 }
142 }
143 Event::KeyUp => {
144 state.event_log.push_back("Key: ↑ Up".to_string());
145 }
146 Event::KeyDown => {
147 state.event_log.push_back("Key: ↓ Down".to_string());
148 }
149 Event::KeyLeft => {
150 state.event_log.push_back("Key: ← Left".to_string());
151 }
152 Event::KeyRight => {
153 state.event_log.push_back("Key: → Right".to_string());
154 }
155 Event::Enter => {
156 state.event_log.push_back("Key: ⏎ Enter".to_string());
157 }
158 Event::Escape => {
159 state.event_log.push_back("Key: Escape".to_string());
160 }
161 Event::Backspace => {
162 state.event_log.push_back("Key: ⌫ Backspace".to_string());
163 }
164 Event::Delete => {
165 state.event_log.push_back("Key: ⌦ Delete".to_string());
166 }
167 Event::FunctionKey(n) => {
168 state.event_log.push_back(format!("Key: F{}", n));
169 }
170
171 // Handle mouse events
172 Event::MouseMove { x, y } => {
173 state.mouse_pos = (x, y);
174 // Only log occasional moves to avoid spam
175 if x % 3 == 0 && y % 3 == 0 {
176 state
177 .event_log
178 .push_back(format!("Mouse: Moved to ({}, {})", x, y));
179 }
180 }
181 Event::MouseClick { x, y, button } => {
182 state.mouse_pos = (x, y);
183 let button_name = match button {
184 MouseButton::Left => "Left",
185 MouseButton::Right => "Right",
186 MouseButton::Middle => "Middle",
187 MouseButton::Other(_) => "Other",
188 };
189
190 // Check for double-click
191 if state.click_tracker.is_double_click(x, y) {
192 state.event_log.push_back(format!(
193 "Mouse: DOUBLE-CLICK! {} button at ({}, {})",
194 button_name, x, y
195 ));
196 } else {
197 state
198 .event_log
199 .push_back(format!("Mouse: {} click at ({}, {})", button_name, x, y));
200 }
201 }
202 Event::MouseDrag { x, y, button } => {
203 state.mouse_pos = (x, y);
204 let button_name = match button {
205 MouseButton::Left => "Left",
206 MouseButton::Right => "Right",
207 MouseButton::Middle => "Middle",
208 MouseButton::Other(_) => "Other",
209 };
210 state
211 .event_log
212 .push_back(format!("Mouse: {} drag to ({}, {})", button_name, x, y));
213 }
214 Event::MouseScroll { delta } => {
215 let direction = if delta > 0 { "up" } else { "down" };
216 state
217 .event_log
218 .push_back(format!("Mouse: Scroll {} ({})", direction, delta));
219 }
220 Event::MouseScrollHorizontal { delta } => {
221 let direction = if delta > 0 { "right" } else { "left" };
222 state
223 .event_log
224 .push_back(format!("Mouse: Scroll {} ({})", direction, delta));
225 }
226 Event::MouseRelease { x, y, button } => {
227 state.mouse_pos = (x, y);
228 let button_name = match button {
229 MouseButton::Left => "Left",
230 MouseButton::Right => "Right",
231 MouseButton::Middle => "Middle",
232 MouseButton::Other(_) => "Other",
233 };
234 state
235 .event_log
236 .push_back(format!("Mouse: {} release at ({}, {})", button_name, x, y));
237 }
238
239 Event::Resize { width, height } => {
240 state
241 .event_log
242 .push_back(format!("Terminal: Resized to {}x{}", width, height));
243 }
244 _ => {}
245 }
246
247 // Keep the log at reasonable size
248 if state.event_log.len() > MAX_EVENTS {
249 state.event_log.pop_front();
250 }
251
252 true
253 },
254 |state, window| {
255 let (term_width, term_height) = window.get_size();
256
257 // Create a container to display the events.
258 //
259 // Panel has been absorbed into Container: use borders + title + padding, and put
260 // content widgets inside as children.
261 // NOTE: `ContainerPadding` is the name exported by the prelude for Container's padding type.
262 // (The underlying type in `container.rs` is `Padding`.)
263 use minui::widgets::ContainerPadding;
264
265 let panel_x: u16 = 2u16;
266 let panel_y: u16 = 1u16;
267 let panel_w: u16 = term_width.saturating_sub(4u16);
268 let panel_h: u16 = term_height.saturating_sub(4u16);
269
270 // Render the log as stacked labels so each event appears on its own line.
271 // We display the newest entries at the top (reverse chronological).
272 let mut log_container = Container::vertical().with_row_gap(Gap::Pixels(0u16));
273 if state.event_log.is_empty() {
274 log_container = log_container.add_child(Label::new("No events yet..."));
275 } else {
276 for line in state.event_log.iter().rev().take(MAX_EVENTS) {
277 log_container = log_container.add_child(Label::new(line.clone()));
278 }
279 }
280
281 let panel = Container::new()
282 .with_position_and_size(panel_x, panel_y, panel_w, panel_h)
283 .with_border()
284 .with_border_chars(BorderChars::double_line())
285 .with_border_color(ColorPair::new(Color::Cyan, Color::Black))
286 .with_title("MinUI Input Demo")
287 .with_title_alignment(TitleAlignment::Center)
288 .with_padding(ContainerPadding::uniform(1u16))
289 .add_child(log_container);
290
291 panel.draw(window)?;
292
293 // Draw mouse position info at bottom
294 let mouse_info = format!("Mouse: ({}, {})", state.mouse_pos.0, state.mouse_pos.1);
295 let info_y = term_height.saturating_sub(2);
296 window.write_str_colored(
297 info_y,
298 2,
299 &mouse_info,
300 ColorPair::new(Color::Cyan, Color::Transparent),
301 )?;
302
303 // Draw instructions at the very bottom
304 let help_text = "Press 'q' to quit | Try typing, clicking, scrolling!";
305 let help_x = (term_width.saturating_sub(help_text.len() as u16)) / 2;
306 let help_y = term_height.saturating_sub(1);
307 window.write_str_colored(
308 help_y,
309 help_x,
310 help_text,
311 ColorPair::new(Color::DarkGray, Color::Transparent),
312 )?;
313
314 window.flush()?;
315 Ok(())
316 },
317 )?;
318
319 Ok(())
320}Sourcepub fn last_position(&self) -> (u16, u16)
pub fn last_position(&self) -> (u16, u16)
Returns the position of the last click.
Sourcepub fn time_since_last_click(&self) -> Duration
pub fn time_since_last_click(&self) -> Duration
Returns the time elapsed since the last click.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for ClickTracker
impl RefUnwindSafe for ClickTracker
impl Send for ClickTracker
impl Sync for ClickTracker
impl Unpin for ClickTracker
impl UnsafeUnpin for ClickTracker
impl UnwindSafe for ClickTracker
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more