Files
Ely-GPUI-Components/src/primitives/pressable.rs
T
ZacharyZhang-NY a13343c2a5 T01 primitives: image, divider, backdrop, focus, pressable, measure, tooltip
Ely-built primitives plus gallery demos for every chapter-one entry,
including gpui natives. The gallery now scripts real input by posting
NSEvents to its own queue and shoots each interaction in both modes.
Codex gpt-6-astra (max): three rounds; the last finding, a two-line
doc comment, was fixed after the three-review cap.
2026-09-25 00:39:06 -04:00

112 lines
3.8 KiB
Rust

use std::{rc::Rc, time::Duration};
use gpui::{
AnyElement, App, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement,
RenderOnce, Task, Window, div,
};
type Handler = Rc<dyn Fn(&mut Window, &mut App)>;
type Content = Box<dyn Fn(bool, &mut Window, &mut App) -> AnyElement>;
const LONG_PRESS: Duration = Duration::from_millis(500);
#[derive(Default)]
struct PressState {
pressed: bool,
long_fired: bool,
_timer: Option<Task<()>>,
}
/// Pressed state and long press for any content.
#[derive(IntoElement)]
pub struct Pressable {
id: ElementId,
content: Content,
on_press: Option<Handler>,
on_long_press: Option<Handler>,
}
impl Pressable {
/// `content` receives whether the pointer is down.
pub fn new(
id: impl Into<ElementId>,
content: impl Fn(bool, &mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
Self {
id: id.into(),
content: Box::new(content),
on_press: None,
on_long_press: None,
}
}
pub fn on_press(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_press = Some(Rc::new(handler));
self
}
pub fn on_long_press(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_long_press = Some(Rc::new(handler));
self
}
}
impl RenderOnce for Pressable {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = window.use_keyed_state(self.id.clone(), cx, |_, _| PressState::default());
let pressed = state.read(cx).pressed;
let content = (self.content)(pressed, window, cx);
let (down, up, out) = (state.clone(), state.clone(), state);
let on_long_press = self.on_long_press;
let on_press = self.on_press;
div()
.id(self.id)
.child(content)
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
let timer = on_long_press.clone().map(|handler| {
let weak = down.downgrade();
window.spawn(cx, async move |cx| {
cx.background_executor().timer(LONG_PRESS).await;
let fired = cx.update(|window, cx| {
weak.update(cx, |press, cx| {
press.long_fired = press.pressed;
cx.notify();
press.long_fired
})
.map(|fire| fire.then(|| handler(window, cx)))
});
if let Err(error) = fired.and_then(|inner| inner) {
log::error!("pressable: long press lost its window: {error:#}");
}
})
});
down.update(cx, |press, cx| {
*press = PressState {
pressed: true,
long_fired: false,
_timer: timer,
};
cx.notify();
});
})
.on_mouse_up(MouseButton::Left, move |_, window, cx| {
let released = up.update(cx, |press, cx| {
let short = press.pressed && !press.long_fired;
*press = PressState::default();
cx.notify();
short
});
if let Some(handler) = on_press.as_ref().filter(|_| released) {
handler(window, cx);
}
})
.on_mouse_up_out(MouseButton::Left, move |_, _, cx| {
out.update(cx, |press, cx| {
*press = PressState::default();
cx.notify();
})
})
}
}