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.
This commit is contained in:
38 files changed
+2165
-157
No files matched your search
@@ -0,0 +1,111 @@
|
||||
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();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user