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:
ZacharyZhang-NY committed 2026-09-25 00:39:06 -04:00
1 parent abf4cbcfc6
commit a13343c2a5
38 files changed
+2165 -157

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
use std::time::Duration;
use gpui::{
Animation, AnimationExt, App, Div, ElementId, ImageSource, InteractiveElement, IntoElement,
ObjectFit, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img,
pulsating_between,
};
use crate::{
primitives::{Icon, IconName},
theme::{ActiveTheme, IconSize},
};
const PULSE: Duration = Duration::from_millis(1600);
/// Themed `img()`; a `Cover` crop keeps square corners.
#[derive(IntoElement)]
pub struct Image {
id: ElementId,
base: Div,
source: Option<ImageSource>,
fit: ObjectFit,
}
impl Image {
/// The id keeps gpui's loading state across frames.
pub fn new(id: impl Into<ElementId>, source: impl Into<ImageSource>) -> Self {
Self {
id: id.into(),
base: div(),
source: Some(source.into()),
fit: ObjectFit::Cover,
}
}
/// Nothing to load yet.
pub fn placeholder(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
base: div(),
source: None,
fit: ObjectFit::Cover,
}
}
pub fn fit(mut self, fit: ObjectFit) -> Self {
self.fit = fit;
self
}
}
impl Styled for Image {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl RenderOnce for Image {
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let (sunken, subtle) = (theme.colors.sunken, theme.colors.fg_subtle);
let still = theme.reduced_motion;
let radii = self.base.style().corner_radii.clone();
let pane = {
let radii = radii.clone();
move || {
let mut pane = div().size_full().bg(sunken);
pane.style().corner_radii = radii.clone();
pane
}
};
let loading_pane = pane.clone();
let mark = move |name| {
pane()
.flex()
.items_center()
.justify_center()
.child(Icon::new(name).size(IconSize::Lg).color(subtle))
};
let Some(source) = self.source else {
return self.base.overflow_hidden().child(mark(IconName::Image));
};
let mut picture = img(source)
.id(self.id)
.size_full()
.object_fit(self.fit)
.with_loading(move || {
if still {
return loading_pane().into_any_element();
}
loading_pane()
.with_animation(
"image-loading",
Animation::new(PULSE)
.repeat()
.with_easing(pulsating_between(0.55, 1.0)),
|pane, t| pane.opacity(t),
)
.into_any_element()
})
.with_fallback(move || mark(IconName::ImageOff).into_any_element());
picture.style().corner_radii = radii;
self.base.overflow_hidden().child(picture)
}
}